1use std::collections::BTreeMap;
16use std::iter::Peekable;
17use std::vec;
18
19use crate::check::normalize;
20use crate::emit::HEADER;
21
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
26pub struct Written {
27 pub elements: BTreeMap<String, WrittenElement>,
28 pub relations: BTreeMap<(String, String), WrittenRelation>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
34pub struct WrittenElement {
35 pub kind: String,
36 pub title: String,
37 pub description: Option<String>,
38 pub technology: Option<String>,
39 pub path: Option<String>,
40 pub tags: Vec<String>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Default)]
44pub struct WrittenRelation {
45 pub kind: String,
46 pub label: Option<String>,
50 pub technology: Option<String>,
51}
52
53pub fn read(text: &str) -> Option<Written> {
56 let text = normalize(text);
57 let rest = text.strip_prefix(HEADER)?.strip_prefix('\n')?;
58 let start = rest.find("\nmodel {\n")? + "\nmodel {\n".len();
59 let mut parser = Parser {
60 tokens: tokenize(&rest[start..])?.into_iter().peekable(),
61 written: Written::default(),
62 };
63 parser.model_body()?;
64 let written = parser.written;
65 written
66 .relations
67 .keys()
68 .all(|(from, to)| written.elements.contains_key(from) && written.elements.contains_key(to))
69 .then_some(written)
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73enum Token {
74 Word(String),
75 Quoted(String),
76 Open,
77 Close,
78}
79
80fn tokenize(text: &str) -> Option<Vec<Token>> {
84 let mut tokens = Vec::new();
85 let mut depth = 1usize;
86 let mut chars = text.chars().peekable();
87 while let Some(c) = chars.next() {
88 match c {
89 '{' => {
90 depth += 1;
91 tokens.push(Token::Open);
92 }
93 '}' => {
94 tokens.push(Token::Close);
95 depth -= 1;
96 if depth == 0 {
97 return Some(tokens);
98 }
99 }
100 '\'' => {
101 let mut value = String::new();
102 loop {
103 match chars.next()? {
104 '\'' => break,
105 '\\' => value.push(chars.next()?),
106 c => value.push(c),
107 }
108 }
109 tokens.push(Token::Quoted(value));
110 }
111 c if c.is_whitespace() => {}
112 c => {
113 let mut word = String::from(c);
114 while let Some(&c) = chars.peek() {
115 if c.is_whitespace() || matches!(c, '{' | '}' | '\'') {
116 break;
117 }
118 word.push(c);
119 chars.next();
120 }
121 tokens.push(Token::Word(word));
122 }
123 }
124 }
125 None
126}
127
128struct Parser {
130 tokens: Peekable<vec::IntoIter<Token>>,
131 written: Written,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
137enum Field {
138 Tags,
139 Description,
140 Technology,
141 Metadata,
142 Link,
143 Children,
144}
145
146impl Field {
147 fn repeats(self) -> bool {
150 matches!(self, Field::Tags | Field::Children)
151 }
152}
153
154impl Parser {
155 fn next(&mut self) -> Option<Token> {
156 self.tokens.next()
157 }
158
159 fn word(&mut self) -> Option<String> {
160 match self.next()? {
161 Token::Word(word) => Some(word),
162 _ => None,
163 }
164 }
165
166 fn quoted(&mut self) -> Option<String> {
167 match self.next()? {
168 Token::Quoted(value) => Some(value),
169 _ => None,
170 }
171 }
172
173 fn expect(&mut self, token: Token) -> Option<()> {
174 (self.next()? == token).then_some(())
175 }
176
177 fn expect_word(&mut self, word: &str) -> Option<()> {
178 (self.word()? == word).then_some(())
179 }
180
181 fn model_body(&mut self) -> Option<()> {
183 let mut in_relations = false;
184 loop {
185 let first = match self.next()? {
186 Token::Close => return Some(()),
187 Token::Word(word) => word,
188 _ => return None,
189 };
190 let second = self.word()?;
191 if second == "=" {
192 if in_relations {
193 return None;
194 }
195 self.element("", first)?;
196 } else {
197 in_relations = true;
198 self.relation(first, &second)?;
199 }
200 }
201 }
202
203 fn element(&mut self, parent: &str, name: String) -> Option<()> {
205 let kind = self.word()?;
206 let title = self.quoted()?;
207 let id = if parent.is_empty() {
208 name
209 } else {
210 format!("{parent}.{name}")
211 };
212 let mut element = WrittenElement {
213 kind,
214 title,
215 ..Default::default()
216 };
217 if self.tokens.peek() == Some(&Token::Open) {
218 self.next();
219 self.element_body(&id, &mut element)?;
220 }
221 self.written
222 .elements
223 .insert(id, element)
224 .is_none()
225 .then_some(())
226 }
227
228 fn element_body(&mut self, id: &str, element: &mut WrittenElement) -> Option<()> {
229 let mut at: Option<Field> = None;
230 loop {
231 let word = match self.next()? {
232 Token::Close => return Some(()),
233 Token::Word(word) => word,
234 _ => return None,
235 };
236 let field = match word.as_str() {
237 w if w.starts_with('#') => Field::Tags,
238 "description" => Field::Description,
239 "technology" => Field::Technology,
240 "metadata" => Field::Metadata,
241 "link" if at == Some(Field::Metadata) => Field::Link,
244 _ => Field::Children,
245 };
246 if at.is_some_and(|at| field < at || (field == at && !field.repeats())) {
247 return None;
248 }
249 at = Some(field);
250 match field {
251 Field::Tags => element.tags.push(word[1..].to_string()),
252 Field::Description => element.description = Some(self.quoted()?),
253 Field::Technology => element.technology = Some(self.quoted()?),
254 Field::Metadata => {
255 self.expect(Token::Open)?;
256 self.expect_word("path")?;
257 element.path = Some(self.quoted()?);
258 self.expect(Token::Close)?;
259 }
260 Field::Link => {
262 self.word()?;
263 }
264 Field::Children => {
265 self.expect_word("=")?;
266 self.element(id, word)?;
267 }
268 }
269 }
270 }
271
272 fn relation(&mut self, from: String, arrow: &str) -> Option<()> {
275 let kind = arrow.strip_prefix("-[")?.strip_suffix("]->")?.to_string();
276 let to = self.word()?;
277 let mut relation = WrittenRelation {
278 kind,
279 ..Default::default()
280 };
281 if matches!(self.tokens.peek(), Some(Token::Quoted(_))) {
282 relation.label = Some(self.quoted()?);
283 }
284 if self.tokens.peek() == Some(&Token::Open) {
285 self.next();
286 self.expect_word("technology")?;
287 relation.technology = Some(self.quoted()?);
288 self.expect(Token::Close)?;
289 }
290 self.written
291 .relations
292 .insert((from, to), relation)
293 .is_none()
294 .then_some(())
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::emit::{EmitOptions, emit};
302 use crate::model::{Element, ElementKind, Model, Relation, RelationKind};
303
304 fn id(dotted: &str) -> Vec<String> {
305 dotted.split('.').map(str::to_string).collect()
306 }
307
308 fn element(dotted: &str, kind: ElementKind) -> Element {
309 Element {
310 id: id(dotted),
311 kind,
312 title: dotted.rsplit('.').next().unwrap().to_string(),
313 description: None,
314 technology: None,
315 path: None,
316 tags: vec![],
317 }
318 }
319
320 fn relation(source: &str, target: &str, kind: RelationKind, items: &[&str]) -> Relation {
321 Relation {
322 source: id(source),
323 target: id(target),
324 kind,
325 items: items.iter().map(|s| s.to_string()).collect(),
326 technology: None,
327 }
328 }
329
330 fn sample() -> Model {
333 let mut app = element("app", ElementKind::Container);
334 app.technology = Some("library crate".into());
335 app.path = Some("app".into());
336 let mut server = element("app.server", ElementKind::Component);
337 server.description =
338 Some("Serves 'quoted' text,\nover two lines, with \\ and {braces}.".into());
339 server.path = Some("app/src/server.rs".into());
340 let mut bin = element("app.main", ElementKind::Bin);
341 bin.tags = vec!["bin".into()];
342 bin.path = Some("app/src/main.rs".into());
343 let leaf = element("app.leaf", ElementKind::Component);
344 let mut driver = element("node_driver", ElementKind::External("process".into()));
345 driver.title = "Driver process".into();
346 driver.tags = vec!["external".into()];
347 driver.technology = Some("Node.js".into());
348 let mut spawns = relation("app.server", "node_driver", RelationKind::Uses, &["spawns"]);
349 spawns.technology = Some("stdio".into());
350 Model {
351 elements: vec![app, server, bin, leaf, driver],
352 relations: vec![
353 relation(
354 "app.main",
355 "app.server",
356 RelationKind::Calls,
357 &["Server", "serve"],
358 ),
359 relation("app.leaf", "app.server", RelationKind::NamesType, &[]),
360 spawns,
361 ],
362 ..Default::default()
363 }
364 }
365
366 fn sample_written() -> Written {
367 read(&emit(
368 &sample(),
369 &EmitOptions::for_output_path("docs/architecture/model.c4"),
370 ))
371 .unwrap()
372 }
373
374 #[test]
375 fn every_element_is_read_back_under_its_full_id() {
376 assert_eq!(
377 sample_written().elements.keys().collect::<Vec<_>>(),
378 ["app", "app.leaf", "app.main", "app.server", "node_driver"]
379 );
380 }
381
382 #[test]
383 fn an_element_s_fields_are_read_back_as_written() {
384 assert_eq!(
385 sample_written().elements["app.server"],
386 WrittenElement {
387 kind: "component".into(),
388 title: "server".into(),
389 description: Some(
390 "Serves 'quoted' text,\nover two lines, with \\ and {braces}.".into()
391 ),
392 technology: None,
393 path: Some("app/src/server.rs".into()),
394 tags: vec![],
395 }
396 );
397 }
398
399 #[test]
400 fn a_crate_s_technology_is_read_back() {
401 assert_eq!(
402 sample_written().elements["app"].technology.as_deref(),
403 Some("library crate")
404 );
405 }
406
407 #[test]
408 fn a_bin_s_tag_is_read_back() {
409 assert_eq!(sample_written().elements["app.main"].tags, ["bin"]);
410 }
411
412 #[test]
413 fn an_external_is_read_back_with_its_own_kind() {
414 assert_eq!(
415 sample_written().elements["node_driver"],
416 WrittenElement {
417 kind: "process".into(),
418 title: "Driver process".into(),
419 description: None,
420 technology: Some("Node.js".into()),
421 path: None,
422 tags: vec!["external".into()],
423 }
424 );
425 }
426
427 #[test]
428 fn a_leaf_with_no_body_is_read_back_with_only_its_kind_and_title() {
429 assert_eq!(
430 sample_written().elements["app.leaf"],
431 WrittenElement {
432 kind: "component".into(),
433 title: "leaf".into(),
434 ..Default::default()
435 }
436 );
437 }
438
439 fn pair(from: &str, to: &str) -> (String, String) {
440 (from.to_string(), to.to_string())
441 }
442
443 #[test]
444 fn a_relation_is_read_back_with_its_kind_and_label() {
445 assert_eq!(
446 sample_written().relations[&pair("app.main", "app.server")],
447 WrittenRelation {
448 kind: "calls".into(),
449 label: Some("Server, serve".into()),
450 technology: None,
451 }
452 );
453 }
454
455 #[test]
456 fn a_relation_without_items_has_no_label() {
457 assert_eq!(
458 sample_written().relations[&pair("app.leaf", "app.server")].label,
459 None
460 );
461 }
462
463 #[test]
464 fn a_relation_s_technology_is_read_back() {
465 assert_eq!(
466 sample_written().relations[&pair("app.server", "node_driver")],
467 WrittenRelation {
468 kind: "uses".into(),
469 label: Some("spawns".into()),
470 technology: Some("stdio".into()),
471 }
472 );
473 }
474
475 #[test]
476 fn an_external_title_holding_a_comma_reads_back_whole() {
477 let mut model = sample();
478 model.relations[2].items = vec!["reads, writes".into()];
479 let written = read(&emit(&model, &EmitOptions::for_output_path("m.c4"))).unwrap();
480 assert_eq!(
481 written.relations[&pair("app.server", "node_driver")]
482 .label
483 .as_deref(),
484 Some("reads, writes")
485 );
486 }
487
488 #[test]
489 fn crlf_line_endings_read_the_same() {
490 let text = emit(&sample(), &EmitOptions::for_output_path("m.c4"));
491 assert_eq!(read(&text.replace('\n', "\r\n")), read(&text));
492 }
493
494 #[test]
495 fn a_file_without_the_header_is_not_read() {
496 let text = emit(&sample(), &EmitOptions::for_output_path("m.c4"));
497 let edited = text.replacen("// GENERATED by asbuilt.", "// mine", 1);
498 assert_eq!(read(&edited), None);
499 }
500
501 #[test]
502 fn a_statement_emit_would_not_write_is_not_read() {
503 let text = emit(&sample(), &EmitOptions::for_output_path("m.c4"));
504 let edited = text.replacen(
505 " technology 'library crate'",
506 " style { color red }",
507 1,
508 );
509 assert_eq!(read(&edited), None);
510 }
511
512 #[test]
513 fn an_unterminated_string_is_not_read() {
514 let text = emit(&sample(), &EmitOptions::for_output_path("m.c4"));
515 let edited = text.replacen("'Driver process'", "'Driver process", 1);
516 assert_eq!(read(&edited), None);
517 }
518
519 #[test]
520 fn a_relation_written_twice_is_not_read() {
521 let text = emit(&sample(), &EmitOptions::for_output_path("m.c4"));
522 let line = " app.main -[calls]-> app.server 'Server, serve'\n";
523 assert_eq!(
524 read(&text.replacen(line, &format!("{line}{line}"), 1)),
525 None
526 );
527 }
528
529 #[test]
530 fn an_element_written_twice_is_not_read() {
531 let text = emit(&sample(), &EmitOptions::for_output_path("m.c4"));
532 let line = " leaf = component 'leaf'\n";
533 assert_eq!(
534 read(&text.replacen(line, &format!("{line}{line}"), 1)),
535 None
536 );
537 }
538
539 fn edited(from: &str, to: &str) -> Option<Written> {
540 let text = emit(&sample(), &EmitOptions::for_output_path("m.c4"));
541 assert!(text.contains(from), "{from:?} not in\n{text}");
542 read(&text.replacen(from, to, 1))
543 }
544
545 #[test]
546 fn a_header_line_with_more_on_it_is_not_read() {
547 assert_eq!(edited("survey.\n", "survey. And mine.\n"), None);
548 }
549
550 #[test]
551 fn fields_out_of_emit_s_order_are_not_read() {
552 assert_eq!(
553 edited(
554 " technology 'library crate'\n metadata {\n path 'app'\n }\n",
555 " metadata {\n path 'app'\n }\n technology 'library crate'\n"
556 ),
557 None
558 );
559 }
560
561 #[test]
562 fn a_field_written_twice_is_not_read() {
563 assert_eq!(
564 edited(
565 " technology 'library crate'\n",
566 " technology 'library crate'\n technology 'binary crate'\n"
567 ),
568 None
569 );
570 }
571
572 #[test]
573 fn a_link_without_the_metadata_before_it_is_not_read() {
574 assert_eq!(
575 edited(" metadata {\n path 'app'\n }\n", ""),
576 None
577 );
578 }
579
580 #[test]
581 fn an_element_after_a_relation_is_not_read() {
582 assert_eq!(
583 edited(
584 " app.main -[calls]-> ",
585 " extra = component 'extra'\n app.main -[calls]-> "
586 ),
587 None
588 );
589 }
590
591 #[test]
592 fn a_relation_to_an_element_the_file_does_not_declare_is_not_read() {
593 let mut model = sample();
596 model.elements.push(element("a.b", ElementKind::Component));
597 model
598 .relations
599 .push(relation("app.leaf", "a.b", RelationKind::Uses, &[]));
600 assert_eq!(
601 read(&emit(&model, &EmitOptions::for_output_path("m.c4"))),
602 None
603 );
604 }
605}