1pub trait Entity {
7 type Update;
8 type Id: Ord + std::hash::Hash + Copy;
9 fn apply(&mut self, update: &Self::Update);
10}
11
12pub fn is_default<T: Default + PartialEq>(value: &T) -> bool {
16 *value == T::default()
17}
18
19macro_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 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 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 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 #[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 #[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 #[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 #[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 #[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}