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(crate) fn line(self) -> &'static str {
117 match self {
118 RelationKind::Implements | RelationKind::Constructs | RelationKind::Calls => "solid",
119 RelationKind::NamesType | RelationKind::Uses => "dotted",
120 }
121 }
122
123 pub(crate) fn head(self) -> &'static str {
131 match self {
132 RelationKind::Implements => "onormal",
133 RelationKind::Constructs => "diamond",
134 RelationKind::Calls => "vee",
135 RelationKind::NamesType => "dot",
136 RelationKind::Uses => "odot",
137 }
138 }
139
140 pub const ALL: [RelationKind; 5] = [
142 RelationKind::Implements,
143 RelationKind::Constructs,
144 RelationKind::Calls,
145 RelationKind::NamesType,
146 RelationKind::Uses,
147 ];
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct Relation {
152 pub source: Id,
153 pub target: Id,
154 pub kind: RelationKind,
155 pub items: Vec<String>,
157 pub technology: Option<String>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Default)]
166pub struct Deployment {
167 pub nodes: Vec<DeploymentNode>,
168 pub instances: Vec<DeploymentInstance>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct DeploymentNode {
173 pub id: Id,
174 pub kind: String,
175 pub title: String,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct DeploymentInstance {
180 pub id: Id,
181 pub element: Id,
183}
184
185pub fn is_lineal(a: &[String], b: &[String]) -> bool {
189 a.starts_with(b) || b.starts_with(a)
190}
191
192pub fn dotted(id: &[String]) -> String {
194 id.join(".")
195}
196
197pub const RESERVED_GRAMMAR_VERSION: &str = "1.59.4";
204
205pub const RESERVED: &[&str] = &[
212 "BottomTop",
213 "LeftRight",
214 "RightLeft",
215 "TopBottom",
216 "and",
217 "autoLayout",
218 "border",
219 "color",
220 "deploymentNode",
221 "description",
222 "dynamic",
223 "dynamicPredicateGroup",
224 "exclude",
225 "extend",
226 "extends",
227 "false",
228 "from",
229 "global",
230 "head",
231 "icon",
232 "iconColor",
233 "iconPosition",
234 "iconSize",
235 "icons",
236 "import",
237 "include",
238 "includeAncestors",
239 "instanceOf",
240 "is",
241 "it",
242 "kind",
243 "likec4lib",
244 "line",
245 "link",
246 "metadata",
247 "multiple",
248 "navigateTo",
249 "not",
250 "notation",
251 "notes",
252 "of",
253 "opacity",
254 "or",
255 "order",
256 "padding",
257 "predicate",
258 "predicateGroup",
259 "rank",
260 "rgb",
261 "rgba",
262 "shape",
263 "size",
264 "specification",
265 "style",
266 "styleGroup",
267 "summary",
268 "tag",
269 "tail",
270 "technology",
271 "textSize",
272 "this",
273 "title",
274 "true",
275 "variant",
276 "view",
277 "views",
278 "where",
279 "with",
280];
281
282pub fn sanitize_segment(segment: &str) -> String {
286 let mut out = String::with_capacity(segment.len() + 1);
287 if segment.chars().next().is_some_and(|c| c.is_ascii_digit()) {
288 out.push('_');
289 }
290 for c in segment.chars() {
291 out.push(if c.is_ascii_alphanumeric() || c == '_' {
292 c
293 } else {
294 '_'
295 });
296 }
297 if RESERVED.contains(&out.as_str()) {
298 out.push('_');
299 }
300 out
301}
302
303pub fn sanitize_id(id: &[String]) -> String {
305 id.iter()
306 .map(|s| sanitize_segment(s))
307 .collect::<Vec<_>>()
308 .join(".")
309}
310
311impl Model {
312 pub fn normalize(&mut self) {
317 self.elements.sort_by(|a, b| a.id.cmp(&b.id));
318
319 let mut merged: BTreeMap<(Id, Id), Relation> = BTreeMap::new();
320 for rel in self.relations.drain(..) {
321 let key = (rel.source.clone(), rel.target.clone());
322 match merged.get_mut(&key) {
323 None => {
324 merged.insert(key, rel);
325 }
326 Some(existing) => {
327 existing.kind = existing.kind.min(rel.kind);
328 existing.items.extend(rel.items);
329 if existing.technology.is_none() {
330 existing.technology = rel.technology;
331 }
332 }
333 }
334 }
335 self.relations = merged
336 .into_values()
337 .map(|mut rel| {
338 rel.items.sort();
339 rel.items.dedup();
340 rel
341 })
342 .collect();
343 }
344
345 pub fn merge(&mut self, other: Model) {
347 self.elements.extend(other.elements);
348 self.relations.extend(other.relations);
349 self.deployment.nodes.extend(other.deployment.nodes);
350 self.deployment.instances.extend(other.deployment.instances);
351 self.normalize();
352 }
353
354 pub fn validate(&self) -> Result<()> {
357 let mut seen: BTreeMap<String, &Id> = BTreeMap::new();
358 for element in &self.elements {
359 let key = sanitize_id(&element.id);
360 if let Some(first) = seen.insert(key.clone(), &element.id) {
361 return Err(Error::DuplicateId {
362 id: key,
363 first: dotted(first),
364 second: dotted(&element.id),
365 });
366 }
367 }
368 Ok(())
369 }
370
371 pub fn element(&self, id: &[String]) -> Option<&Element> {
372 self.elements.iter().find(|e| e.id == id)
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 fn id(s: &str) -> Id {
381 s.split('.').map(str::to_string).collect()
382 }
383
384 fn element(s: &str) -> Element {
385 Element {
386 id: id(s),
387 kind: ElementKind::Component,
388 title: s.rsplit('.').next().unwrap().to_string(),
389 description: None,
390 technology: None,
391 path: None,
392 tags: vec![],
393 }
394 }
395
396 fn relation(source: &str, target: &str, kind: RelationKind, items: &[&str]) -> Relation {
397 Relation {
398 source: id(source),
399 target: id(target),
400 kind,
401 items: items.iter().map(|s| s.to_string()).collect(),
402 technology: None,
403 }
404 }
405
406 #[test]
407 fn precedence_is_the_declared_order_strongest_first() {
408 assert!(RelationKind::Implements < RelationKind::Constructs);
409 assert!(RelationKind::Constructs < RelationKind::Calls);
410 assert!(RelationKind::Calls < RelationKind::NamesType);
411 assert!(RelationKind::NamesType < RelationKind::Uses);
412 }
413
414 #[test]
415 fn the_surveyed_kinds_are_every_kind_but_external_in_specification_order() {
416 assert_eq!(
417 SURVEY_KINDS.map(|kind| kind.keyword().to_string()),
418 ["container", "component", "bin", "tests", "examples"]
419 );
420 }
421
422 #[test]
423 fn every_kind_has_a_distinct_keyword() {
424 let mut keywords: Vec<_> = RelationKind::ALL.iter().map(|k| k.keyword()).collect();
425 keywords.sort();
426 keywords.dedup();
427 assert_eq!(
428 keywords,
429 ["calls", "constructs", "implements", "names", "uses"]
430 );
431 }
432
433 #[test]
434 fn an_ancestor_and_its_descendant_are_lineal_in_both_directions() {
435 assert!(is_lineal(&id("a"), &id("a.b.c")));
436 assert!(is_lineal(&id("a.b.c"), &id("a")));
437 }
438
439 #[test]
440 fn an_element_is_lineal_with_itself() {
441 assert!(is_lineal(&id("a.b"), &id("a.b")));
442 }
443
444 #[test]
445 fn siblings_are_not_lineal() {
446 assert!(!is_lineal(&id("a.b"), &id("a.c")));
447 }
448
449 #[test]
450 fn a_shared_prefix_string_is_not_lineage() {
451 assert!(!is_lineal(&id("a.b"), &id("a.bc")));
453 }
454
455 #[test]
456 fn a_hyphen_becomes_an_underscore() {
457 assert_eq!(sanitize_segment("playwright-rs"), "playwright_rs");
458 }
459
460 #[test]
461 fn a_leading_digit_is_prefixed() {
462 assert_eq!(sanitize_segment("3d"), "_3d");
463 }
464
465 #[test]
466 fn a_reserved_word_gets_a_trailing_underscore() {
467 assert_eq!(sanitize_segment("view"), "view_");
468 assert_eq!(sanitize_segment("link"), "link_");
469 assert_eq!(sanitize_segment("icons"), "icons_");
471 assert_eq!(sanitize_segment("summary"), "summary_");
472 }
473
474 #[test]
475 fn model_and_element_are_not_reserved() {
476 assert_eq!(sanitize_segment("model"), "model");
479 assert_eq!(sanitize_segment("element"), "element");
480 }
481
482 #[test]
483 fn the_reserved_list_is_sorted_and_unique() {
484 let mut sorted = RESERVED.to_vec();
485 sorted.sort_unstable();
486 sorted.dedup();
487 assert_eq!(sorted, RESERVED);
488 }
489
490 #[test]
491 fn a_plain_identifier_is_unchanged() {
492 assert_eq!(sanitize_segment("server_v2"), "server_v2");
493 }
494
495 #[test]
496 fn sanitizing_an_id_keeps_the_dots() {
497 assert_eq!(sanitize_id(&id("play-wright.a.1x")), "play_wright.a._1x");
498 }
499
500 #[test]
501 fn normalize_sorts_elements_by_id() {
502 let mut model = Model {
503 elements: vec![element("b"), element("a.z"), element("a")],
504 ..Default::default()
505 };
506 model.normalize();
507 let ids: Vec<_> = model.elements.iter().map(|e| dotted(&e.id)).collect();
508 assert_eq!(ids, ["a", "a.z", "b"]);
509 }
510
511 #[test]
512 fn normalize_merges_relations_on_the_pair_keeping_the_strongest_kind() {
513 let mut model = Model {
514 relations: vec![
515 relation("a", "b", RelationKind::Uses, &["Y"]),
516 relation("a", "b", RelationKind::Calls, &["X"]),
517 ],
518 ..Default::default()
519 };
520 model.normalize();
521 assert_eq!(
522 model.relations,
523 vec![relation("a", "b", RelationKind::Calls, &["X", "Y"])]
524 );
525 }
526
527 #[test]
528 fn normalize_dedups_items_within_a_relation() {
529 let mut model = Model {
530 relations: vec![relation("a", "b", RelationKind::Uses, &["Y", "X", "Y"])],
531 ..Default::default()
532 };
533 model.normalize();
534 assert_eq!(model.relations[0].items, ["X", "Y"]);
535 }
536
537 #[test]
538 fn normalize_sorts_relations_by_source_then_target() {
539 let mut model = Model {
540 relations: vec![
541 relation("b", "a", RelationKind::Uses, &[]),
542 relation("a", "c", RelationKind::Uses, &[]),
543 relation("a", "b", RelationKind::Uses, &[]),
544 ],
545 ..Default::default()
546 };
547 model.normalize();
548 let pairs: Vec<_> = model
549 .relations
550 .iter()
551 .map(|r| (dotted(&r.source), dotted(&r.target)))
552 .collect();
553 assert_eq!(
554 pairs,
555 [
556 ("a".into(), "b".into()),
557 ("a".into(), "c".into()),
558 ("b".into(), "a".into())
559 ]
560 );
561 }
562
563 #[test]
564 fn normalize_keeps_the_first_technology_seen() {
565 let mut first = relation("a", "b", RelationKind::Uses, &[]);
566 first.technology = Some("stdio".into());
567 let mut model = Model {
568 relations: vec![
569 relation("a", "b", RelationKind::Uses, &[]),
570 first,
571 relation("a", "b", RelationKind::Uses, &[]),
572 ],
573 ..Default::default()
574 };
575 model.normalize();
576 assert_eq!(model.relations[0].technology.as_deref(), Some("stdio"));
577 }
578
579 #[test]
580 fn merge_appends_and_normalizes() {
581 let mut model = Model {
582 elements: vec![element("b")],
583 ..Default::default()
584 };
585 model.merge(Model {
586 elements: vec![element("a")],
587 relations: vec![relation("a", "b", RelationKind::Uses, &["X"])],
588 ..Default::default()
589 });
590 let ids: Vec<_> = model.elements.iter().map(|e| dotted(&e.id)).collect();
591 assert_eq!(ids, ["a", "b"]);
592 assert_eq!(model.relations.len(), 1);
593 }
594
595 #[test]
596 fn two_ids_that_sanitize_alike_are_a_duplicate_naming_both() {
597 let model = Model {
598 elements: vec![element("foo-bar"), element("foo_bar")],
599 ..Default::default()
600 };
601 match model.validate() {
602 Err(Error::DuplicateId { id, first, second }) => {
603 assert_eq!(id, "foo_bar");
604 assert_eq!(first, "foo-bar");
605 assert_eq!(second, "foo_bar");
606 }
607 other => panic!("expected DuplicateId, got {other:?}"),
608 }
609 }
610
611 #[test]
612 fn an_id_that_appears_twice_is_a_duplicate() {
613 let model = Model {
614 elements: vec![element("app"), element("app")],
615 ..Default::default()
616 };
617 match model.validate() {
618 Err(Error::DuplicateId { id, first, second }) => {
619 assert_eq!(
620 (id.as_str(), first.as_str(), second.as_str()),
621 ("app", "app", "app")
622 );
623 }
624 other => panic!("expected DuplicateId, got {other:?}"),
625 }
626 }
627
628 #[test]
629 fn distinct_ids_validate() {
630 let model = Model {
631 elements: vec![element("a"), element("a.b"), element("b")],
632 ..Default::default()
633 };
634 assert!(model.validate().is_ok());
635 }
636
637 #[test]
638 fn element_finds_by_exact_id() {
639 let model = Model {
640 elements: vec![element("a"), element("a.b")],
641 ..Default::default()
642 };
643 assert_eq!(
644 model.element(&id("a.b")).map(|e| e.title.as_str()),
645 Some("b")
646 );
647 assert_eq!(model.element(&id("a.b.c")), None);
648 }
649
650 #[test]
651 fn each_relation_kind_is_drawn_with_its_line_and_head() {
652 assert_eq!(
653 RelationKind::ALL.map(|kind| (kind.keyword(), kind.line(), kind.head())),
654 [
655 ("implements", "solid", "onormal"),
656 ("constructs", "solid", "diamond"),
657 ("calls", "solid", "vee"),
658 ("names", "dotted", "dot"),
659 ("uses", "dotted", "odot"),
660 ]
661 );
662 }
663}