1use std::collections::BTreeMap;
11
12use crate::error::{Error, Result};
13
14pub type Id = Vec<String>;
16
17#[derive(Debug, Clone, PartialEq, Eq, Default)]
19pub struct Model {
20 pub elements: Vec<Element>,
21 pub relations: Vec<Relation>,
22 pub deployment: Deployment,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Element {
27 pub id: Id,
28 pub kind: ElementKind,
29 pub title: String,
30 pub description: Option<String>,
32 pub technology: Option<String>,
35 pub path: Option<String>,
38 pub tags: Vec<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum ElementKind {
44 Container,
46 Component,
48 Bin,
51 Tests,
54 Examples,
56 External(String),
59}
60
61pub const SURVEY_KINDS: [ElementKind; 5] = [
64 ElementKind::Container,
65 ElementKind::Component,
66 ElementKind::Bin,
67 ElementKind::Tests,
68 ElementKind::Examples,
69];
70
71impl ElementKind {
72 pub fn keyword(&self) -> &str {
76 match self {
77 ElementKind::Container => "container",
78 ElementKind::Component => "component",
79 ElementKind::Bin => "bin",
80 ElementKind::Tests => "tests",
81 ElementKind::Examples => "examples",
82 ElementKind::External(name) => name,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
91pub enum RelationKind {
92 Implements,
93 Constructs,
94 Calls,
95 NamesType,
96 Uses,
97}
98
99impl RelationKind {
100 pub fn keyword(self) -> &'static str {
102 match self {
103 RelationKind::Implements => "implements",
104 RelationKind::Constructs => "constructs",
105 RelationKind::Calls => "calls",
106 RelationKind::NamesType => "names",
107 RelationKind::Uses => "uses",
108 }
109 }
110
111 pub const ALL: [RelationKind; 5] = [
113 RelationKind::Implements,
114 RelationKind::Constructs,
115 RelationKind::Calls,
116 RelationKind::NamesType,
117 RelationKind::Uses,
118 ];
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct Relation {
123 pub source: Id,
124 pub target: Id,
125 pub kind: RelationKind,
126 pub items: Vec<String>,
128 pub technology: Option<String>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Default)]
137pub struct Deployment {
138 pub nodes: Vec<DeploymentNode>,
139 pub instances: Vec<DeploymentInstance>,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct DeploymentNode {
144 pub id: Id,
145 pub kind: String,
146 pub title: String,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct DeploymentInstance {
151 pub id: Id,
152 pub element: Id,
154}
155
156pub fn is_lineal(a: &[String], b: &[String]) -> bool {
160 a.starts_with(b) || b.starts_with(a)
161}
162
163pub fn dotted(id: &[String]) -> String {
165 id.join(".")
166}
167
168pub const RESERVED_GRAMMAR_VERSION: &str = "1.59.4";
175
176pub const RESERVED: &[&str] = &[
183 "BottomTop",
184 "LeftRight",
185 "RightLeft",
186 "TopBottom",
187 "and",
188 "autoLayout",
189 "border",
190 "color",
191 "deploymentNode",
192 "description",
193 "dynamic",
194 "dynamicPredicateGroup",
195 "exclude",
196 "extend",
197 "extends",
198 "false",
199 "from",
200 "global",
201 "head",
202 "icon",
203 "iconColor",
204 "iconPosition",
205 "iconSize",
206 "icons",
207 "import",
208 "include",
209 "includeAncestors",
210 "instanceOf",
211 "is",
212 "it",
213 "kind",
214 "likec4lib",
215 "line",
216 "link",
217 "metadata",
218 "multiple",
219 "navigateTo",
220 "not",
221 "notation",
222 "notes",
223 "of",
224 "opacity",
225 "or",
226 "order",
227 "padding",
228 "predicate",
229 "predicateGroup",
230 "rank",
231 "rgb",
232 "rgba",
233 "shape",
234 "size",
235 "specification",
236 "style",
237 "styleGroup",
238 "summary",
239 "tag",
240 "tail",
241 "technology",
242 "textSize",
243 "this",
244 "title",
245 "true",
246 "variant",
247 "view",
248 "views",
249 "where",
250 "with",
251];
252
253pub fn sanitize_segment(segment: &str) -> String {
257 let mut out = String::with_capacity(segment.len() + 1);
258 if segment.chars().next().is_some_and(|c| c.is_ascii_digit()) {
259 out.push('_');
260 }
261 for c in segment.chars() {
262 out.push(if c.is_ascii_alphanumeric() || c == '_' {
263 c
264 } else {
265 '_'
266 });
267 }
268 if RESERVED.contains(&out.as_str()) {
269 out.push('_');
270 }
271 out
272}
273
274pub fn sanitize_id(id: &[String]) -> String {
276 id.iter()
277 .map(|s| sanitize_segment(s))
278 .collect::<Vec<_>>()
279 .join(".")
280}
281
282impl Model {
283 pub fn normalize(&mut self) {
288 self.elements.sort_by(|a, b| a.id.cmp(&b.id));
289
290 let mut merged: BTreeMap<(Id, Id), Relation> = BTreeMap::new();
291 for rel in self.relations.drain(..) {
292 let key = (rel.source.clone(), rel.target.clone());
293 match merged.get_mut(&key) {
294 None => {
295 merged.insert(key, rel);
296 }
297 Some(existing) => {
298 existing.kind = existing.kind.min(rel.kind);
299 existing.items.extend(rel.items);
300 if existing.technology.is_none() {
301 existing.technology = rel.technology;
302 }
303 }
304 }
305 }
306 self.relations = merged
307 .into_values()
308 .map(|mut rel| {
309 rel.items.sort();
310 rel.items.dedup();
311 rel
312 })
313 .collect();
314 }
315
316 pub fn merge(&mut self, other: Model) {
318 self.elements.extend(other.elements);
319 self.relations.extend(other.relations);
320 self.deployment.nodes.extend(other.deployment.nodes);
321 self.deployment.instances.extend(other.deployment.instances);
322 self.normalize();
323 }
324
325 pub fn validate(&self) -> Result<()> {
328 let mut seen: BTreeMap<String, &Id> = BTreeMap::new();
329 for element in &self.elements {
330 let key = sanitize_id(&element.id);
331 if let Some(first) = seen.insert(key.clone(), &element.id) {
332 return Err(Error::DuplicateId {
333 id: key,
334 first: dotted(first),
335 second: dotted(&element.id),
336 });
337 }
338 }
339 Ok(())
340 }
341
342 pub fn element(&self, id: &[String]) -> Option<&Element> {
343 self.elements.iter().find(|e| e.id == id)
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 fn id(s: &str) -> Id {
352 s.split('.').map(str::to_string).collect()
353 }
354
355 fn element(s: &str) -> Element {
356 Element {
357 id: id(s),
358 kind: ElementKind::Component,
359 title: s.rsplit('.').next().unwrap().to_string(),
360 description: None,
361 technology: None,
362 path: None,
363 tags: vec![],
364 }
365 }
366
367 fn relation(source: &str, target: &str, kind: RelationKind, items: &[&str]) -> Relation {
368 Relation {
369 source: id(source),
370 target: id(target),
371 kind,
372 items: items.iter().map(|s| s.to_string()).collect(),
373 technology: None,
374 }
375 }
376
377 #[test]
378 fn precedence_is_the_declared_order_strongest_first() {
379 assert!(RelationKind::Implements < RelationKind::Constructs);
380 assert!(RelationKind::Constructs < RelationKind::Calls);
381 assert!(RelationKind::Calls < RelationKind::NamesType);
382 assert!(RelationKind::NamesType < RelationKind::Uses);
383 }
384
385 #[test]
386 fn the_surveyed_kinds_are_every_kind_but_external_in_specification_order() {
387 assert_eq!(
388 SURVEY_KINDS.map(|kind| kind.keyword().to_string()),
389 ["container", "component", "bin", "tests", "examples"]
390 );
391 }
392
393 #[test]
394 fn every_kind_has_a_distinct_keyword() {
395 let mut keywords: Vec<_> = RelationKind::ALL.iter().map(|k| k.keyword()).collect();
396 keywords.sort();
397 keywords.dedup();
398 assert_eq!(
399 keywords,
400 ["calls", "constructs", "implements", "names", "uses"]
401 );
402 }
403
404 #[test]
405 fn an_ancestor_and_its_descendant_are_lineal_in_both_directions() {
406 assert!(is_lineal(&id("a"), &id("a.b.c")));
407 assert!(is_lineal(&id("a.b.c"), &id("a")));
408 }
409
410 #[test]
411 fn an_element_is_lineal_with_itself() {
412 assert!(is_lineal(&id("a.b"), &id("a.b")));
413 }
414
415 #[test]
416 fn siblings_are_not_lineal() {
417 assert!(!is_lineal(&id("a.b"), &id("a.c")));
418 }
419
420 #[test]
421 fn a_shared_prefix_string_is_not_lineage() {
422 assert!(!is_lineal(&id("a.b"), &id("a.bc")));
424 }
425
426 #[test]
427 fn a_hyphen_becomes_an_underscore() {
428 assert_eq!(sanitize_segment("playwright-rs"), "playwright_rs");
429 }
430
431 #[test]
432 fn a_leading_digit_is_prefixed() {
433 assert_eq!(sanitize_segment("3d"), "_3d");
434 }
435
436 #[test]
437 fn a_reserved_word_gets_a_trailing_underscore() {
438 assert_eq!(sanitize_segment("view"), "view_");
439 assert_eq!(sanitize_segment("link"), "link_");
440 assert_eq!(sanitize_segment("icons"), "icons_");
442 assert_eq!(sanitize_segment("summary"), "summary_");
443 }
444
445 #[test]
446 fn model_and_element_are_not_reserved() {
447 assert_eq!(sanitize_segment("model"), "model");
450 assert_eq!(sanitize_segment("element"), "element");
451 }
452
453 #[test]
454 fn the_reserved_list_is_sorted_and_unique() {
455 let mut sorted = RESERVED.to_vec();
456 sorted.sort_unstable();
457 sorted.dedup();
458 assert_eq!(sorted, RESERVED);
459 }
460
461 #[test]
462 fn a_plain_identifier_is_unchanged() {
463 assert_eq!(sanitize_segment("server_v2"), "server_v2");
464 }
465
466 #[test]
467 fn sanitizing_an_id_keeps_the_dots() {
468 assert_eq!(sanitize_id(&id("play-wright.a.1x")), "play_wright.a._1x");
469 }
470
471 #[test]
472 fn normalize_sorts_elements_by_id() {
473 let mut model = Model {
474 elements: vec![element("b"), element("a.z"), element("a")],
475 ..Default::default()
476 };
477 model.normalize();
478 let ids: Vec<_> = model.elements.iter().map(|e| dotted(&e.id)).collect();
479 assert_eq!(ids, ["a", "a.z", "b"]);
480 }
481
482 #[test]
483 fn normalize_merges_relations_on_the_pair_keeping_the_strongest_kind() {
484 let mut model = Model {
485 relations: vec![
486 relation("a", "b", RelationKind::Uses, &["Y"]),
487 relation("a", "b", RelationKind::Calls, &["X"]),
488 ],
489 ..Default::default()
490 };
491 model.normalize();
492 assert_eq!(
493 model.relations,
494 vec![relation("a", "b", RelationKind::Calls, &["X", "Y"])]
495 );
496 }
497
498 #[test]
499 fn normalize_dedups_items_within_a_relation() {
500 let mut model = Model {
501 relations: vec![relation("a", "b", RelationKind::Uses, &["Y", "X", "Y"])],
502 ..Default::default()
503 };
504 model.normalize();
505 assert_eq!(model.relations[0].items, ["X", "Y"]);
506 }
507
508 #[test]
509 fn normalize_sorts_relations_by_source_then_target() {
510 let mut model = Model {
511 relations: vec![
512 relation("b", "a", RelationKind::Uses, &[]),
513 relation("a", "c", RelationKind::Uses, &[]),
514 relation("a", "b", RelationKind::Uses, &[]),
515 ],
516 ..Default::default()
517 };
518 model.normalize();
519 let pairs: Vec<_> = model
520 .relations
521 .iter()
522 .map(|r| (dotted(&r.source), dotted(&r.target)))
523 .collect();
524 assert_eq!(
525 pairs,
526 [
527 ("a".into(), "b".into()),
528 ("a".into(), "c".into()),
529 ("b".into(), "a".into())
530 ]
531 );
532 }
533
534 #[test]
535 fn normalize_keeps_the_first_technology_seen() {
536 let mut first = relation("a", "b", RelationKind::Uses, &[]);
537 first.technology = Some("stdio".into());
538 let mut model = Model {
539 relations: vec![
540 relation("a", "b", RelationKind::Uses, &[]),
541 first,
542 relation("a", "b", RelationKind::Uses, &[]),
543 ],
544 ..Default::default()
545 };
546 model.normalize();
547 assert_eq!(model.relations[0].technology.as_deref(), Some("stdio"));
548 }
549
550 #[test]
551 fn merge_appends_and_normalizes() {
552 let mut model = Model {
553 elements: vec![element("b")],
554 ..Default::default()
555 };
556 model.merge(Model {
557 elements: vec![element("a")],
558 relations: vec![relation("a", "b", RelationKind::Uses, &["X"])],
559 ..Default::default()
560 });
561 let ids: Vec<_> = model.elements.iter().map(|e| dotted(&e.id)).collect();
562 assert_eq!(ids, ["a", "b"]);
563 assert_eq!(model.relations.len(), 1);
564 }
565
566 #[test]
567 fn two_ids_that_sanitize_alike_are_a_duplicate_naming_both() {
568 let model = Model {
569 elements: vec![element("foo-bar"), element("foo_bar")],
570 ..Default::default()
571 };
572 match model.validate() {
573 Err(Error::DuplicateId { id, first, second }) => {
574 assert_eq!(id, "foo_bar");
575 assert_eq!(first, "foo-bar");
576 assert_eq!(second, "foo_bar");
577 }
578 other => panic!("expected DuplicateId, got {other:?}"),
579 }
580 }
581
582 #[test]
583 fn an_id_that_appears_twice_is_a_duplicate() {
584 let model = Model {
585 elements: vec![element("app"), element("app")],
586 ..Default::default()
587 };
588 match model.validate() {
589 Err(Error::DuplicateId { id, first, second }) => {
590 assert_eq!(
591 (id.as_str(), first.as_str(), second.as_str()),
592 ("app", "app", "app")
593 );
594 }
595 other => panic!("expected DuplicateId, got {other:?}"),
596 }
597 }
598
599 #[test]
600 fn distinct_ids_validate() {
601 let model = Model {
602 elements: vec![element("a"), element("a.b"), element("b")],
603 ..Default::default()
604 };
605 assert!(model.validate().is_ok());
606 }
607
608 #[test]
609 fn element_finds_by_exact_id() {
610 let model = Model {
611 elements: vec![element("a"), element("a.b")],
612 ..Default::default()
613 };
614 assert_eq!(
615 model.element(&id("a.b")).map(|e| e.title.as_str()),
616 Some("b")
617 );
618 assert_eq!(model.element(&id("a.b.c")), None);
619 }
620}