1use serde::{Deserialize, Serialize};
10
11use super::enums::{LabelSide, PinType};
12use super::error::SchemaError;
13
14#[expect(clippy::trivially_copy_pass_by_ref)]
16fn is_false(b: &bool) -> bool {
17 !*b
18}
19fn is_empty_str(s: &str) -> bool {
20 s.is_empty()
21}
22#[expect(clippy::trivially_copy_pass_by_ref)]
23fn is_zero_i32(v: &i32) -> bool {
24 *v == 0
25}
26#[expect(clippy::trivially_copy_pass_by_ref)]
27fn is_zero_f32(v: &f32) -> bool {
28 *v == 0.0
29}
30
31pub const CURRENT_VERSION: u32 = 2;
39
40pub fn pre_versioning() -> u32 {
44 1
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct Document {
50 #[serde(default = "pre_versioning")]
51 pub version: u32,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub name: Option<String>,
56 pub top: String,
57 pub blocks: Vec<Block>,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub routes: Vec<Route>,
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
65 pub texts: Vec<Text>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub areas: Vec<Area>,
68 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub images: Vec<Image>,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
74 pub assets: Vec<Asset>,
75}
76
77pub fn unsupported_version(version: u32) -> Option<String> {
82 (version > CURRENT_VERSION).then(|| {
83 format!(
84 "document format version {version} is newer than this build reads \
85 (up to {CURRENT_VERSION})"
86 )
87 })
88}
89
90#[derive(Clone, Copy)]
94pub struct ScopeContents<'a> {
95 pub routes: &'a [Route],
96 pub texts: &'a [Text],
97 pub areas: &'a [Area],
98 pub images: &'a [Image],
99}
100
101impl Document {
102 pub fn root_contents(&self) -> ScopeContents<'_> {
103 ScopeContents {
104 routes: &self.routes,
105 texts: &self.texts,
106 areas: &self.areas,
107 images: &self.images,
108 }
109 }
110
111 pub fn referenced_assets(&self) -> Vec<&str> {
114 let mut seen = Vec::new();
115 let placements = self
116 .blocks
117 .iter()
118 .flat_map(|b| b.images.iter().chain(b.icon.as_ref()))
119 .chain(&self.images);
120 for id in placements.map(|i| i.asset.as_str()) {
121 if !seen.contains(&id) {
122 seen.push(id);
123 }
124 }
125 seen
126 }
127
128 #[expect(clippy::expect_used, clippy::missing_panics_doc)]
134 pub fn to_json(&self) -> String {
135 serde_json::to_string_pretty(self).expect("the document model serializes infallibly")
136 }
137
138 pub fn parse_json(src: &str, src_name: &str) -> Result<Document, SchemaError> {
144 let parsed: Document =
145 serde_json::from_str(src).map_err(|e| SchemaError::json(src, src_name, &e))?;
146 match unsupported_version(parsed.version) {
147 Some(message) => Err(SchemaError::Json {
148 message,
149 span: (0, src.len().min(1)).into(),
150 src: miette::NamedSource::new(src_name, src.to_owned()),
151 }),
152 None => Ok(parsed),
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct Block {
159 pub id: String,
160 pub x: i32,
161 pub y: i32,
162 pub w: u32,
163 pub h: u32,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub role: Option<u8>,
166 #[serde(default, skip_serializing_if = "is_false")]
167 pub locked: bool,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub title: Option<Label>,
170 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
171 pub type_label: Option<Label>,
172 #[serde(default, skip_serializing_if = "Vec::is_empty")]
173 pub pins: Vec<Pin>,
174 #[serde(default, skip_serializing_if = "Vec::is_empty")]
175 pub routes: Vec<Route>,
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
177 pub texts: Vec<Text>,
178 #[serde(default, skip_serializing_if = "Vec::is_empty")]
179 pub areas: Vec<Area>,
180 #[serde(default, skip_serializing_if = "Vec::is_empty")]
181 pub images: Vec<Image>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub icon: Option<Image>,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub children: Vec<String>,
188}
189
190impl Block {
191 pub fn contents(&self) -> ScopeContents<'_> {
192 ScopeContents {
193 routes: &self.routes,
194 texts: &self.texts,
195 areas: &self.areas,
196 images: &self.images,
197 }
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct Label {
205 #[serde(default, skip_serializing_if = "is_empty_str")]
206 pub name: String,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub side: Option<LabelSide>,
209 #[serde(default, skip_serializing_if = "is_zero_f32")]
210 pub offset: f32,
211 #[serde(default, skip_serializing_if = "is_false")]
212 pub hidden: bool,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216pub struct Pin {
217 pub id: String,
218 #[serde(default, skip_serializing_if = "is_empty_str")]
219 pub name: String,
220 #[serde(rename = "type", default, skip_serializing_if = "is_empty_str")]
221 pub type_label: String,
222 #[serde(default, skip_serializing_if = "is_empty_str")]
223 pub tag: String,
224 #[serde(default, skip_serializing_if = "is_false")]
225 pub tag_hidden: bool,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub loc: Option<String>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub x: Option<i32>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub y: Option<i32>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub w: Option<u32>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub dir: Option<PinType>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub pin_accent: Option<u8>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub port_accent: Option<u8>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub port_pin_accent: Option<u8>,
247 #[serde(default, skip_serializing_if = "is_false")]
249 pub fliplr: bool,
250}
251
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255pub struct Route {
256 #[serde(default, skip_serializing_if = "is_empty_str")]
257 pub name: String,
258 pub from: String,
260 pub to: String,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub role: Option<u8>,
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub waypoints: Vec<Waypoint>,
265 #[serde(default, skip_serializing_if = "Vec::is_empty")]
267 pub labels: Vec<f32>,
268}
269
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct Waypoint {
272 pub x: i32,
273 pub y: i32,
274 #[serde(default, skip_serializing_if = "is_false")]
275 pub locked: bool,
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct Text {
280 pub text: String,
281 #[serde(default, skip_serializing_if = "is_zero_i32")]
282 pub x: i32,
283 #[serde(default, skip_serializing_if = "is_zero_i32")]
284 pub y: i32,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub role: Option<u8>,
287}
288
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290pub struct Area {
291 pub x: i32,
292 pub y: i32,
293 pub w: u32,
294 pub h: u32,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub role: Option<u8>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub title: Option<Label>,
299}
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub struct Image {
306 pub asset: String,
308 pub x: f32,
311 pub y: f32,
312 pub w: f32,
313 pub h: f32,
314}
315
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
318pub struct Asset {
319 pub id: String,
320 #[serde(flatten)]
321 pub image: ImageData,
322}
323
324#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328#[serde(rename_all = "lowercase")]
329pub enum ImageData {
330 Svg(String),
331 Png(String),
332}