1use std::str::FromStr;
9
10use miette::{NamedSource, SourceSpan};
11
12use super::error::SchemaError;
13use super::kdl::{self, Entry, Node, Span, Value};
14use super::model as m;
15
16const RECT_ID_PREFIX: char = 'b';
21const PIN_ID_PREFIX: char = 'p';
22
23fn id_has_form(s: &str, prefix: char) -> bool {
27 s.strip_prefix(prefix)
28 .is_some_and(|digits| digits.parse::<usize>().is_ok())
29}
30
31fn is_asset_id(s: &str) -> bool {
39 !s.is_empty()
40 && s != "."
41 && s != ".."
42 && s.chars()
43 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
44}
45
46pub fn parse(src: &str, src_name: &str) -> Result<m::Document, SchemaError> {
47 let ctx = Ctx {
48 name: src_name,
49 src,
50 };
51 let nodes = kdl::parse(src).map_err(|e| ctx.err(&e.span, e.message, "here"))?;
52 ctx.document(&nodes)
53}
54
55struct Ctx<'a> {
56 name: &'a str,
57 src: &'a str,
58}
59
60impl Ctx<'_> {
61 fn err(
62 &self,
63 span: &Span,
64 message: impl Into<String>,
65 label: impl Into<String>,
66 ) -> SchemaError {
67 SchemaError::Kdl {
68 message: message.into(),
69 span: SourceSpan::from((span.start, span.len())),
70 label: label.into(),
71 src: NamedSource::new(self.name, self.src.to_string()),
72 }
73 }
74
75 fn as_string<'e>(&self, e: &'e Entry) -> Result<&'e str, SchemaError> {
78 e.value
79 .as_str()
80 .ok_or_else(|| self.err(&e.span, "expected a string", "not a string"))
81 }
82
83 fn as_i32(&self, e: &Entry) -> Result<i32, SchemaError> {
84 match &e.value {
85 Value::Int(i) => i32::try_from(*i)
86 .map_err(|_| self.err(&e.span, "integer out of range for i32", "too large")),
87 _ => Err(self.err(&e.span, "expected an integer", "not an integer")),
88 }
89 }
90
91 fn as_u32(&self, e: &Entry) -> Result<u32, SchemaError> {
92 match &e.value {
93 Value::Int(i) => u32::try_from(*i)
94 .map_err(|_| self.err(&e.span, "expected a non-negative integer", "out of range")),
95 _ => Err(self.err(&e.span, "expected an integer", "not an integer")),
96 }
97 }
98
99 fn as_u8(&self, e: &Entry) -> Result<u8, SchemaError> {
100 match &e.value {
101 Value::Int(i) => u8::try_from(*i)
102 .map_err(|_| self.err(&e.span, "expected an integer 0..=255", "out of range")),
103 _ => Err(self.err(&e.span, "expected an integer", "not an integer")),
104 }
105 }
106
107 fn as_f32(&self, e: &Entry) -> Result<f32, SchemaError> {
108 match &e.value {
109 Value::Float(f) => Ok(*f as f32),
110 Value::Int(i) => Ok(*i as f32),
111 _ => Err(self.err(&e.span, "expected a number", "not a number")),
112 }
113 }
114
115 fn as_bool(&self, e: &Entry) -> Result<bool, SchemaError> {
116 match &e.value {
117 Value::Bool(b) => Ok(*b),
118 _ => Err(self.err(&e.span, "expected `true` or `false`", "not a boolean")),
119 }
120 }
121
122 fn as_enum<T: FromStr<Err = String>>(&self, e: &Entry) -> Result<T, SchemaError> {
123 let s = self.as_string(e)?;
124 s.parse()
125 .map_err(|reason: String| self.err(&e.span, reason, "invalid value"))
126 }
127
128 fn req_arg<'n>(
131 &self,
132 node: &'n Node,
133 idx: usize,
134 what: &str,
135 ) -> Result<&'n Entry, SchemaError> {
136 node.arg(idx)
137 .ok_or_else(|| self.err(&node.name_span, format!("missing {what}"), "here"))
138 }
139
140 fn req_prop<'n>(&self, node: &'n Node, key: &str) -> Result<&'n Entry, SchemaError> {
141 node.prop(key)
142 .map(|p| &p.value)
143 .ok_or_else(|| self.err(&node.name_span, format!("missing `{key}`"), "here"))
144 }
145
146 fn opt_u8(&self, node: &Node, key: &str) -> Result<Option<u8>, SchemaError> {
147 node.prop(key).map(|p| self.as_u8(&p.value)).transpose()
148 }
149
150 fn opt_string(&self, node: &Node, key: &str) -> Result<Option<String>, SchemaError> {
151 node.prop(key)
152 .map(|p| self.as_string(&p.value).map(str::to_string))
153 .transpose()
154 }
155
156 fn opt_bool(&self, node: &Node, key: &str) -> Result<bool, SchemaError> {
157 Ok(match node.prop(key) {
158 Some(p) => self.as_bool(&p.value)?,
159 None => false,
160 })
161 }
162
163 fn opt_enum<T: FromStr<Err = String>>(
164 &self,
165 node: &Node,
166 key: &str,
167 ) -> Result<Option<T>, SchemaError> {
168 node.prop(key).map(|p| self.as_enum(&p.value)).transpose()
169 }
170
171 fn id_str(&self, e: &Entry, kind: &str, prefix: char) -> Result<String, SchemaError> {
174 let s = self.as_string(e)?;
175 if id_has_form(s, prefix) {
176 Ok(s.to_string())
177 } else {
178 Err(self.err(
179 &e.span,
180 format!(
181 "{kind} id {s:?} must be `{prefix}<N>` (the `{prefix}` prefix and a number)"
182 ),
183 "invalid id",
184 ))
185 }
186 }
187
188 fn asset_id(&self, e: &Entry) -> Result<String, SchemaError> {
189 let s = self.as_string(e)?;
190 if is_asset_id(s) {
191 Ok(s.to_string())
192 } else {
193 Err(self.err(
194 &e.span,
195 format!("asset id {s:?} is not usable as a file name"),
196 "invalid id",
197 ))
198 }
199 }
200
201 fn anchor(&self, e: &Entry) -> Result<String, SchemaError> {
202 let s = self.as_string(e)?;
203 let ok = match s.split_once(':') {
204 Some((b, p)) => id_has_form(b, RECT_ID_PREFIX) && id_has_form(p, PIN_ID_PREFIX),
205 None => id_has_form(s, PIN_ID_PREFIX),
206 };
207 if ok {
208 Ok(s.to_string())
209 } else {
210 Err(self.err(
211 &e.span,
212 format!("route anchor {s:?} must be `p<N>` (a port) or `b<N>:p<M>` (a child pin)"),
213 "invalid anchor",
214 ))
215 }
216 }
217
218 fn document(&self, nodes: &[Node]) -> Result<m::Document, SchemaError> {
221 let version = self.version(nodes)?;
225
226 let mut name: Option<String> = None;
227 let mut top: Option<String> = None;
228 let mut blocks = Vec::new();
229 let mut assets: Vec<m::Asset> = Vec::new();
230 for node in nodes {
231 match node.name.as_str() {
232 "version" => {}
233 "name" => {
234 let e = self.req_arg(node, 0, "the document name")?;
235 name = Some(self.as_string(e)?.to_string());
236 }
237 "top" => {
238 let e = self.req_arg(node, 0, "the top block id")?;
239 top = Some(self.id_str(e, "block", RECT_ID_PREFIX)?);
240 }
241 "block" => blocks.push(self.block(node)?),
242 "asset" => {
243 let asset = self.asset(node)?;
244 if assets.iter().any(|a| a.id == asset.id) {
245 return Err(self.err(
246 &node.name_span,
247 format!("duplicate asset id {:?}", asset.id),
248 "already defined",
249 ));
250 }
251 assets.push(asset);
252 }
253 other => {
254 return Err(self.err(
255 &node.name_span,
256 format!(
257 "unexpected top-level node `{other}` \
258 (expected `version`, `name`, `top`, `block` or `asset`)"
259 ),
260 "unexpected node",
261 ));
262 }
263 }
264 }
265 let top = top.ok_or_else(|| self.err(&(0..0), "missing `top` block id", "here"))?;
266 Ok(m::Document {
267 version,
268 name,
269 top,
270 blocks,
271 routes: Vec::new(),
274 texts: Vec::new(),
275 areas: Vec::new(),
276 images: Vec::new(),
277 assets,
278 })
279 }
280
281 fn version(&self, nodes: &[Node]) -> Result<u32, SchemaError> {
285 let Some(node) = nodes.iter().find(|n| n.name == "version") else {
286 return Ok(m::pre_versioning());
287 };
288 let e = self.req_arg(node, 0, "the format version")?;
289 let version = self.as_u32(e)?;
290 match m::unsupported_version(version) {
291 Some(why) => Err(self.err(&e.span, why, "written by a newer blockworx")),
292 None => Ok(version),
293 }
294 }
295
296 fn block(&self, node: &Node) -> Result<m::Block, SchemaError> {
297 let id = self.id_str(
298 self.req_arg(node, 0, "the block id")?,
299 "block",
300 RECT_ID_PREFIX,
301 )?;
302 let mut b = m::Block {
303 id,
304 x: self.as_i32(self.req_prop(node, "x")?)?,
305 y: self.as_i32(self.req_prop(node, "y")?)?,
306 w: self.as_u32(self.req_prop(node, "w")?)?,
307 h: self.as_u32(self.req_prop(node, "h")?)?,
308 role: self.opt_u8(node, "role")?,
309 locked: self.opt_bool(node, "locked")?,
310 title: None,
311 type_label: None,
312 pins: Vec::new(),
313 routes: Vec::new(),
314 texts: Vec::new(),
315 areas: Vec::new(),
316 images: Vec::new(),
317 icon: None,
318 children: Vec::new(),
319 };
320 for child in &node.children {
321 match child.name.as_str() {
322 "title" => b.title = Some(self.label(child)?),
323 "type" => b.type_label = Some(self.label(child)?),
324 "pin" => b.pins.push(self.pin(child)?),
325 "route" => b.routes.push(self.route(child)?),
326 "text" => b.texts.push(self.text(child)?),
327 "area" | "comment" => b.areas.push(self.area(child)?),
330 "image" => b.images.push(self.image(child)?),
331 "icon" => b.icon = Some(self.image(child)?),
332 "children" => {
333 for e in &child.args {
334 b.children.push(self.id_str(e, "block", RECT_ID_PREFIX)?);
335 }
336 }
337 other => {
338 return Err(self.err(
339 &child.name_span,
340 format!("unexpected node `{other}` in a block"),
341 "unexpected node",
342 ));
343 }
344 }
345 }
346 Ok(b)
347 }
348
349 fn label(&self, node: &Node) -> Result<m::Label, SchemaError> {
350 Ok(m::Label {
351 name: match node.arg(0) {
352 Some(e) => self.as_string(e)?.to_string(),
353 None => String::new(),
354 },
355 side: self.opt_enum(node, "side")?,
356 offset: match node.prop("offset") {
357 Some(p) => self.as_f32(&p.value)?,
358 None => 0.0,
359 },
360 hidden: self.opt_bool(node, "hidden")?,
361 })
362 }
363
364 fn pin(&self, node: &Node) -> Result<m::Pin, SchemaError> {
365 let id = self.id_str(self.req_arg(node, 0, "the pin id")?, "pin", PIN_ID_PREFIX)?;
366 let loc_entry = self.req_prop(node, "loc")?;
367 let loc_str = self.as_string(loc_entry)?;
368 super::loc::parse_loc(loc_str)
369 .map_err(|reason| self.err(&loc_entry.span, reason, "invalid loc"))?;
370 let loc = Some(loc_str.to_string());
371 Ok(m::Pin {
372 id,
373 name: match node.arg(1) {
374 Some(e) => self.as_string(e)?.to_string(),
375 None => String::new(),
376 },
377 type_label: self.opt_string(node, "type")?.unwrap_or_default(),
378 tag: self.opt_string(node, "tag")?.unwrap_or_default(),
379 tag_hidden: self.opt_bool(node, "tag-hidden")?,
380 loc,
381 x: node.prop("x").map(|p| self.as_i32(&p.value)).transpose()?,
382 y: node.prop("y").map(|p| self.as_i32(&p.value)).transpose()?,
383 w: node.prop("w").map(|p| self.as_u32(&p.value)).transpose()?,
384 dir: self.opt_enum(node, "dir")?,
385 pin_accent: self.opt_u8(node, "pin-accent")?,
386 port_accent: self.opt_u8(node, "port-accent")?,
387 port_pin_accent: self.opt_u8(node, "port-pin-accent")?,
388 fliplr: self.opt_bool(node, "fliplr")?,
389 })
390 }
391
392 fn route(&self, node: &Node) -> Result<m::Route, SchemaError> {
393 let from = self.anchor(self.req_arg(node, 0, "the route's `from` anchor")?)?;
394 let to = self.anchor(self.req_arg(node, 1, "the route's `to` anchor")?)?;
395 let mut waypoints = Vec::new();
396 let mut labels = Vec::new();
397 for child in &node.children {
398 match child.name.as_str() {
399 "wp" => waypoints.push(m::Waypoint {
400 x: self.as_i32(self.req_arg(child, 0, "the waypoint x")?)?,
401 y: self.as_i32(self.req_arg(child, 1, "the waypoint y")?)?,
402 locked: self.opt_bool(child, "locked")?,
403 }),
404 "label" => {
405 labels.push(self.as_f32(self.req_arg(child, 0, "the label position")?)?);
406 }
407 other => {
408 return Err(self.err(
409 &child.name_span,
410 format!("unexpected node `{other}` in a route"),
411 "unexpected node",
412 ));
413 }
414 }
415 }
416 Ok(m::Route {
417 name: self.opt_string(node, "name")?.unwrap_or_default(),
418 from,
419 to,
420 role: self.opt_u8(node, "role")?,
421 waypoints,
422 labels,
423 })
424 }
425
426 fn text(&self, node: &Node) -> Result<m::Text, SchemaError> {
427 Ok(m::Text {
428 text: self
429 .as_string(self.req_arg(node, 0, "the text content")?)?
430 .to_string(),
431 x: self.opt_i32(node, "x")?,
432 y: self.opt_i32(node, "y")?,
433 role: self.opt_u8(node, "role")?,
434 })
435 }
436
437 fn area(&self, node: &Node) -> Result<m::Area, SchemaError> {
438 Ok(m::Area {
439 x: self.as_i32(self.req_prop(node, "x")?)?,
440 y: self.as_i32(self.req_prop(node, "y")?)?,
441 w: self.as_u32(self.req_prop(node, "w")?)?,
442 h: self.as_u32(self.req_prop(node, "h")?)?,
443 role: self.opt_u8(node, "role")?,
444 title: node.child("title").map(|c| self.label(c)).transpose()?,
445 })
446 }
447
448 fn image(&self, node: &Node) -> Result<m::Image, SchemaError> {
451 if let Some(inline) = node.child("svg").or_else(|| node.child("png")) {
452 return Err(self.err(
453 &inline.name_span,
454 "inline image data is no longer supported",
455 "move it to a top-level `asset` node and reference it by id",
456 ));
457 }
458 Ok(m::Image {
459 asset: self.asset_id(self.req_arg(node, 0, "the asset id")?)?,
460 x: self.as_f32(self.req_prop(node, "x")?)?,
461 y: self.as_f32(self.req_prop(node, "y")?)?,
462 w: self.as_f32(self.req_prop(node, "w")?)?,
463 h: self.as_f32(self.req_prop(node, "h")?)?,
464 })
465 }
466
467 fn asset(&self, node: &Node) -> Result<m::Asset, SchemaError> {
469 let id = self.asset_id(self.req_arg(node, 0, "the asset id")?)?;
470 let image = match (node.child("svg"), node.child("png")) {
471 (Some(_), Some(png)) => {
472 return Err(self.err(
473 &png.name_span,
474 "an asset has both `svg` and `png`",
475 "remove one",
476 ));
477 }
478 (Some(svg), None) => m::ImageData::Svg(
479 self.as_string(self.req_arg(svg, 0, "the svg source")?)?
480 .to_string(),
481 ),
482 (None, Some(png)) => m::ImageData::Png(
483 self.as_string(self.req_arg(png, 0, "the png data")?)?
484 .to_string(),
485 ),
486 (None, None) => {
487 return Err(self.err(
488 &node.name_span,
489 "an asset needs an `svg` or `png` child",
490 "here",
491 ));
492 }
493 };
494 Ok(m::Asset { id, image })
495 }
496
497 fn opt_i32(&self, node: &Node, key: &str) -> Result<i32, SchemaError> {
498 Ok(match node.prop(key) {
499 Some(p) => self.as_i32(&p.value)?,
500 None => 0,
501 })
502 }
503}