Skip to main content

blockworx_doc/
entity.rs

1//! The [`Entity`] trait and the `entity!` macro that implements it: one
2//! field list per kind (`block_model.rs`) expands to the struct, its
3//! update vocabulary, and the trait impl — the mirror guard is
4//! generation, not transcription.
5
6pub trait Entity {
7    type Update;
8    type Id: Ord + std::hash::Hash + Copy;
9    fn apply(&mut self, update: &Self::Update);
10}
11
12/// Whether a value is its own zero — the suppression predicate every
13/// field's `skip_serializing_if` names, so "anything at its default is
14/// omitted" is one rule rather than one per field type.
15pub fn is_default<T: Default + PartialEq>(value: &T) -> bool {
16    *value == T::default()
17}
18
19/// One entity kind from one field list: the struct and its `Update` enum,
20/// plus the [`Entity`] impl. Field classes: `registers` (writable
21/// leaves), `namespaces` (nested vocabularies addressed by descent),
22/// `constants` (creation-time values — no update variant). Serde variant
23/// names on the update enum are wire tags: written explicitly, never
24/// derived from field names (`FlipLR` is not `FlipLr`), never renamed or
25/// repurposed — deprecate and add instead. The struct's *field* names are
26/// the document format's keys.
27macro_rules! entity {
28    (
29        $(#[$meta:meta])*
30        pub struct $entity:ident(update $update:ident, id $id:ty) {
31            registers { $( $(#[$rmeta:meta])* $rvar:ident => $rfield:ident : $rty:ty ),* $(,)? }
32            namespaces { $( $nvar:ident => $nfield:ident : $nty:ty ),* $(,)? }
33            constants { $( $(#[$cmeta:meta])* $cfield:ident : $cty:ty ),* $(,)? }
34        }
35    ) => {
36        $(#[$meta])*
37        #[derive(
38            serde::Serialize, serde::Deserialize, Debug, Default, Clone, PartialEq,
39        )]
40        pub struct $entity {
41            $(
42                $(#[$rmeta])*
43                #[serde(default, skip_serializing_if = "crate::entity::is_default")]
44                pub $rfield: $rty,
45            )*
46            $(
47                #[serde(default, skip_serializing_if = "crate::entity::is_default")]
48                pub $nfield: $nty,
49            )*
50            $(
51                $(#[$cmeta])*
52                #[serde(default, skip_serializing_if = "crate::entity::is_default")]
53                pub $cfield: $cty,
54            )*
55        }
56
57        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
58        pub enum $update {
59            $( $rvar($rty), )*
60            $( $nvar(<$nty as $crate::entity::Entity>::Update), )*
61        }
62
63        impl $crate::entity::Entity for $entity {
64            type Update = $update;
65            type Id = $id;
66            fn apply(&mut self, update: &Self::Update) {
67                match update {
68                    $( $update::$rvar(value) => self.$rfield = value.clone(), )*
69                    $( $update::$nvar(value) => self.$nfield.apply(value), )*
70                }
71            }
72        }
73    };
74}
75pub(crate) use entity;
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::fixtures::{block_id, pin_id, route_id};
81    use crate::{
82        block_model::{
83            Area, AreaUpdate, Block, BlockUpdate, Icon, Image, ImageUpdate, Label, LabelUpdate,
84            Pin, PinUpdate, Route, RouteLabel, RouteLabelUpdate, RouteUpdate, Text, TextUpdate,
85        },
86        geometry::{
87            FracVal, GridPoint, GridRect, GridSize, PinSlot, ScreenPoint, ScreenRect, ScreenSize,
88            Waypoint,
89        },
90        hash::AssetHash,
91        values::{LabelSide, PinDir, PinSide, Role},
92    };
93
94    fn rect(x: i32, y: i32, w: u32, h: u32) -> GridRect {
95        GridRect {
96            top_left: GridPoint { x, y },
97            size: GridSize { w, h },
98        }
99    }
100
101    fn screen_rect(x: f32, y: f32) -> ScreenRect {
102        ScreenRect {
103            top_left: ScreenPoint {
104                x: FracVal::from(x),
105                y: FracVal::from(y),
106            },
107            size: ScreenSize {
108                w: FracVal::from(8.0),
109                h: FracVal::from(8.0),
110            },
111        }
112    }
113
114    /// Every field deliberately non-default, so a round trip that drops a
115    /// value for `Default::default()` cannot pass.
116    fn label(name: &str) -> Label {
117        Label {
118            name: name.into(),
119            side: LabelSide::Bottom,
120            offset: FracVal::from(1.5),
121            hidden: true,
122        }
123    }
124
125    fn distinct_block() -> Block {
126        Block {
127            parent: block_id(1),
128            rect: rect(2, 3, 4, 5),
129            locked: true,
130            role: Role::Accent6,
131            title: label("title"),
132            type_label: label("type"),
133            icon: Icon {
134                asset: AssetHash::of(b"icon"),
135                rect: screen_rect(1.0, 2.0),
136            },
137        }
138    }
139
140    /// Every register a different value, so a walk cannot pass by reading
141    /// a neighbour.
142    fn distinct_pin() -> Pin {
143        Pin {
144            owner: block_id(1),
145            name: "clk".into(),
146            type_name: "clock".into(),
147            tag: "t0".into(),
148            tag_hidden: true,
149            rect: rect(1, 1, 2, 2),
150            slot: PinSlot {
151                side: PinSide::East,
152                offset: 7,
153            },
154            dir: PinDir::Output,
155            port_accent: Role::Accent4,
156            flip_lr: true,
157        }
158    }
159
160    fn distinct_route() -> Route {
161        Route {
162            owner: block_id(1),
163            name: "net7".into(),
164            from: pin_id(2),
165            to: pin_id(3),
166            role: Role::Accent2,
167            waypoints: vec![Waypoint {
168                pos: GridPoint { x: 3, y: 4 },
169                locked: true,
170            }],
171        }
172    }
173
174    /// The entity struct *is* the document format (`docs/json-format.md`),
175    /// so its serde is proved the way the format is read: a value with no
176    /// field at its zero survives the trip unchanged. A predicate that
177    /// suppressed a field carrying a value would lose it here.
178    fn round_trips<E>(entity: &E)
179    where
180        E: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
181    {
182        let text = serde_json::to_string(entity).expect("an entity serializes");
183        assert_eq!(
184            &serde_json::from_str::<E>(&text).expect("and parses back"),
185            entity,
186        );
187    }
188
189    #[test]
190    fn every_kind_round_trips_through_json() {
191        round_trips(&distinct_block());
192        round_trips(&distinct_pin());
193        round_trips(&distinct_route());
194        round_trips(&RouteLabel {
195            owner: route_id(1),
196            pos: FracVal::from(0.25),
197        });
198        round_trips(&Text {
199            owner: block_id(1),
200            text: "note".into(),
201            pos: GridPoint { x: 5, y: 6 },
202            role: Role::Accent1,
203            width: std::num::NonZeroU32::new(12),
204        });
205        round_trips(&Area {
206            owner: block_id(1),
207            rect: rect(0, 0, 3, 3),
208            role: Role::Accent6,
209            title: label("area"),
210        });
211        round_trips(&Image {
212            owner: block_id(1),
213            asset: AssetHash::of(b"png"),
214            rect: screen_rect(3.0, 4.0),
215        });
216    }
217
218    /// The suppression rule, end to end: an entity at its zero writes no
219    /// keys at all, and reads back as itself.
220    #[test]
221    fn an_entity_at_its_zero_writes_nothing() {
222        assert_eq!(
223            serde_json::to_string(&Block::default()).expect("it serializes"),
224            "{}",
225        );
226        round_trips(&Block::default());
227    }
228
229    /// One value off its zero is the only key written, so a document file
230    /// says what was authored and not what every default happens to be.
231    #[test]
232    fn only_the_fields_off_their_zero_are_written() {
233        let moved = Block {
234            rect: rect(2, 3, 4, 5),
235            ..Block::default()
236        };
237        assert_eq!(
238            serde_json::to_string(&moved).expect("it serializes"),
239            r#"{"rect":{"top_left":{"x":2,"y":3},"size":{"w":4,"h":5}}}"#,
240        );
241    }
242
243    /// The descent-mistargeting hazard: `Title` and `TypeLabel` share the
244    /// `LabelUpdate` vocabulary, so the test must prove the update landed in
245    /// the named namespace and not its twin.
246    #[test]
247    fn namespace_descent_lands_in_the_named_label() {
248        let mut block = distinct_block();
249        block.apply(&BlockUpdate::Title(LabelUpdate::Name("renamed".into())));
250        assert_eq!(block.title.name.as_str(), "renamed");
251        assert_eq!(
252            block.type_label.name.as_str(),
253            "type",
254            "the twin namespace must be untouched"
255        );
256        assert_eq!(
257            block.title.side,
258            LabelSide::Bottom,
259            "sibling registers in the named namespace must be untouched"
260        );
261    }
262
263    #[test]
264    fn apply_lands_each_update_in_its_register() {
265        let mut pin = distinct_pin();
266        pin.apply(&PinUpdate::Dir(PinDir::InOut));
267        assert_eq!(pin.dir, PinDir::InOut);
268        assert_eq!(pin.name.as_str(), "clk", "untouched registers stand");
269
270        let mut route = distinct_route();
271        let dragged = vec![Waypoint {
272            pos: GridPoint { x: 9, y: 9 },
273            locked: false,
274        }];
275        route.apply(&RouteUpdate::Waypoints(dragged.clone()));
276        assert_eq!(route.waypoints, dragged);
277    }
278
279    /// Every write wins: an update lands whatever the entity held, which
280    /// is what replaced the last-write-wins comparison.
281    #[test]
282    fn the_last_write_stands() {
283        let mut block = distinct_block();
284        block.apply(&BlockUpdate::Rect(rect(9, 9, 9, 9)));
285        block.apply(&BlockUpdate::Rect(rect(1, 1, 1, 1)));
286        assert_eq!(block.rect, rect(1, 1, 1, 1));
287    }
288
289    /// The remaining kinds' vocabularies, walked once so a variant that
290    /// names the wrong field is caught where it is generated.
291    #[test]
292    fn the_small_kinds_apply_their_own_registers() {
293        let mut route_label = RouteLabel {
294            owner: route_id(1),
295            pos: FracVal::from(0.25),
296        };
297        route_label.apply(&RouteLabelUpdate::Pos(FracVal::from(0.5)));
298        assert_eq!(route_label.pos, FracVal::from(0.5));
299
300        let mut text = Text {
301            owner: block_id(1),
302            text: "note".into(),
303            pos: GridPoint { x: 5, y: 6 },
304            role: Role::Accent1,
305            width: None,
306        };
307        text.apply(&TextUpdate::Pos(GridPoint { x: 9, y: 9 }));
308        assert_eq!(text.pos, GridPoint { x: 9, y: 9 });
309        assert_eq!(text.text.as_str(), "note");
310        text.apply(&TextUpdate::Width(std::num::NonZeroU32::new(20)));
311        assert_eq!(text.width, std::num::NonZeroU32::new(20));
312        assert_eq!(text.pos, GridPoint { x: 9, y: 9 });
313
314        let mut area = Area {
315            owner: block_id(1),
316            rect: rect(0, 0, 3, 3),
317            role: Role::Accent6,
318            title: label("area"),
319        };
320        area.apply(&AreaUpdate::Title(LabelUpdate::Hidden(false)));
321        assert!(!area.title.hidden);
322        assert_eq!(area.role, Role::Accent6);
323
324        let mut image = Image {
325            owner: block_id(1),
326            asset: AssetHash::of(b"png"),
327            rect: screen_rect(3.0, 4.0),
328        };
329        image.apply(&ImageUpdate::Asset(AssetHash::of(b"jpg")));
330        assert_eq!(image.asset, AssetHash::of(b"jpg"));
331    }
332}