1use crate::diagnostic::{Diagnostic, Tolerance};
18use crate::version::BcfVersion;
19use crate::xml::Node;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct HeaderFile {
24 pub ifc_project: Option<String>,
26 pub ifc_spatial_structure_element: Option<String>,
28 pub filename: Option<String>,
30 pub date: Option<String>,
32 pub reference: Option<String>,
34 pub is_external: Option<bool>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ViewPointRef {
47 pub guid: Option<String>,
49 pub viewpoint: Option<String>,
51 pub snapshot: Option<String>,
53 pub index: Option<i32>,
55 pub visualization: Option<Visualization>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65#[non_exhaustive]
66pub struct Visualization {
67 pub guid: Option<String>,
69 pub selection: Vec<Component>,
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
81pub struct Component {
82 pub ifc_guid: Option<String>,
84 pub originating_system: Option<String>,
86 pub authoring_tool_id: Option<String>,
88}
89
90impl Component {
91 #[must_use]
93 pub fn ifc(ifc_guid: impl Into<String>) -> Self {
94 Self {
95 ifc_guid: Some(ifc_guid.into()),
96 ..Self::default()
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Comment {
104 pub guid: Option<String>,
106 pub date: Option<String>,
108 pub author: Option<String>,
110 pub comment: Option<String>,
112 pub viewpoint: Option<String>,
114 pub modified_date: Option<String>,
116 pub modified_author: Option<String>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct Topic {
123 pub guid: Option<String>,
126 pub topic_type: Option<String>,
128 pub topic_status: Option<String>,
130 pub server_assigned_id: Option<String>,
132 pub title: Option<String>,
134 pub priority: Option<String>,
136 pub index: Option<i32>,
138 pub labels: Vec<String>,
140 pub creation_date: Option<String>,
142 pub creation_author: Option<String>,
144 pub modified_date: Option<String>,
146 pub modified_author: Option<String>,
148 pub due_date: Option<String>,
150 pub assigned_to: Option<String>,
152 pub description: Option<String>,
154 pub stage: Option<String>,
156 pub related_topics: Vec<String>,
158 pub reference_links: Vec<String>,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct Markup {
165 pub entry: String,
167 pub header_files: Vec<HeaderFile>,
169 pub topic: Topic,
171 pub comments: Vec<Comment>,
173 pub viewpoints: Vec<ViewPointRef>,
175}
176
177impl Markup {
178 #[must_use]
180 pub fn title(&self) -> &str {
181 self.topic.title.as_deref().unwrap_or("")
182 }
183
184 #[must_use]
186 pub fn status(&self) -> Option<&str> {
187 self.topic.topic_status.as_deref()
188 }
189}
190
191pub(crate) fn observe_version(markup: &Node) -> Option<BcfVersion> {
202 let topic = markup.child("Topic")?;
203
204 let nests_collections = topic.child("Comments").is_some()
215 || topic.child("Viewpoints").is_some()
216 || topic
217 .child("Labels")
218 .is_some_and(|l| l.child("Label").is_some())
219 || topic
220 .child("DocumentReferences")
221 .is_some_and(|d| d.child("DocumentReference").is_some())
222 || topic
223 .child("ReferenceLinks")
224 .is_some_and(|r| r.child("ReferenceLink").is_some());
225 if nests_collections || topic.attr("ServerAssignedId").is_some() {
226 return Some(BcfVersion::V3_0);
227 }
228 if markup
232 .children_named("Viewpoints")
233 .any(|v| v.child("ViewPoint").is_some())
234 {
235 return Some(BcfVersion::V3_0);
236 }
237
238 let comments_are_2_0 = markup.children_named("Comment").any(|c| {
243 c.child("Topic").is_some()
244 || c.child("Status").is_some()
245 || c.child("VerbalStatus").is_some()
246 || c.child("ReplyToComment").is_some()
247 });
248 if comments_are_2_0 {
249 return Some(BcfVersion::V2_0);
250 }
251
252 None
258}
259
260pub(crate) fn interpret(
263 entry: &str,
264 root: &Node,
265 diagnostics: &mut Vec<Diagnostic>,
266) -> Option<Markup> {
267 let Some(topic_node) = root.child("Topic") else {
268 diagnostics.push(Diagnostic::in_entry(entry, Tolerance::MarkupWithoutTopic));
269 return None;
270 };
271
272 let topic = read_topic(entry, topic_node, diagnostics);
273
274 let header_files = root
275 .child("Header")
276 .into_iter()
277 .flat_map(|h| h.children_named("File"))
278 .map(read_header_file)
279 .collect();
280
281 let mut viewpoints: Vec<ViewPointRef> = Vec::new();
288 for wrapper in root
289 .children_named("Viewpoints")
290 .chain(topic_node.children_named("Viewpoints"))
291 {
292 if wrapper.child("ViewPoint").is_some() {
293 viewpoints.extend(wrapper.children_named("ViewPoint").map(read_viewpoint));
294 } else if !wrapper.children.is_empty() || wrapper.attr("Guid").is_some() {
295 viewpoints.push(read_viewpoint(wrapper));
298 }
299 }
300
301 let comments = root
303 .children_named("Comment")
304 .chain(
305 topic_node
306 .child("Comments")
307 .into_iter()
308 .flat_map(|c| c.children_named("Comment")),
309 )
310 .map(|c| read_comment(entry, c, diagnostics))
311 .collect();
312
313 Some(Markup {
314 entry: entry.to_string(),
315 header_files,
316 topic,
317 comments,
318 viewpoints,
319 })
320}
321
322fn owned(v: Option<&str>) -> Option<String> {
323 v.map(str::to_string)
324}
325
326fn read_topic(entry: &str, node: &Node, diagnostics: &mut Vec<Diagnostic>) -> Topic {
327 let guid = owned(node.attr("Guid").map(str::trim).filter(|g| !g.is_empty()));
328 if guid.is_none() {
329 diagnostics.push(Diagnostic::in_entry(entry, Tolerance::TopicWithoutGuid));
330 }
331 let title = owned(node.child_text("Title"));
332 if title.is_none() {
333 diagnostics.push(Diagnostic::in_entry(entry, Tolerance::TopicWithoutTitle));
334 }
335
336 Topic {
337 guid,
338 topic_type: owned(node.attr_or_child_text("TopicType")),
339 topic_status: owned(node.attr_or_child_text("TopicStatus")),
340 server_assigned_id: owned(node.attr_or_child_text("ServerAssignedId")),
341 title,
342 priority: owned(node.child_text("Priority")),
343 index: node.child_text("Index").and_then(|i| i.parse().ok()),
344 labels: node
346 .children_named("Labels")
347 .flat_map(|l| {
348 let nested: Vec<&str> = l
349 .children_named("Label")
350 .map(|n| n.text.trim())
351 .filter(|t| !t.is_empty())
352 .collect();
353 if nested.is_empty() {
354 let own = l.text.trim();
355 if own.is_empty() {
356 Vec::new()
357 } else {
358 vec![own]
359 }
360 } else {
361 nested
362 }
363 })
364 .map(str::to_string)
365 .collect(),
366 creation_date: owned(node.child_text("CreationDate")),
367 creation_author: owned(node.child_text("CreationAuthor")),
368 modified_date: owned(node.child_text("ModifiedDate")),
369 modified_author: owned(node.child_text("ModifiedAuthor")),
370 due_date: owned(node.child_text("DueDate")),
371 assigned_to: owned(node.child_text("AssignedTo")),
372 description: owned(node.child_text("Description")),
373 stage: owned(node.child_text("Stage")),
374 related_topics: node
375 .children_named("RelatedTopic")
376 .chain(
377 node.child("RelatedTopics")
378 .into_iter()
379 .flat_map(|r| r.children_named("RelatedTopic")),
380 )
381 .filter_map(|r| owned(r.attr("Guid")))
382 .collect(),
383 reference_links: node
384 .children_named("ReferenceLink")
385 .chain(
386 node.child("ReferenceLinks")
387 .into_iter()
388 .flat_map(|r| r.children_named("ReferenceLink")),
389 )
390 .filter_map(|r| {
391 let t = r.text.trim();
392 (!t.is_empty()).then(|| t.to_string())
393 })
394 .collect(),
395 }
396}
397
398fn read_header_file(node: &Node) -> HeaderFile {
399 HeaderFile {
400 ifc_project: owned(node.attr("IfcProject")),
401 ifc_spatial_structure_element: owned(node.attr("IfcSpatialStructureElement")),
402 filename: owned(node.child_text("Filename")),
403 date: owned(node.child_text("Date")),
404 reference: owned(node.child_text("Reference")),
405 is_external: node.attr("IsExternal").and_then(parse_bool),
406 }
407}
408
409fn read_viewpoint(node: &Node) -> ViewPointRef {
410 ViewPointRef {
411 guid: owned(node.attr("Guid")),
412 viewpoint: owned(node.child_text("Viewpoint")),
413 snapshot: owned(node.child_text("Snapshot")),
414 index: node.child_text("Index").and_then(|i| i.parse().ok()),
415 visualization: None,
417 }
418}
419
420pub(crate) fn interpret_visualization(root: &Node) -> Result<Visualization, String> {
428 if root.name != "VisualizationInfo" {
429 return Err(format!(
430 "root element is <{}>, not <VisualizationInfo>",
431 root.name
432 ));
433 }
434 let components = root.child("Components");
435 let selection = components
439 .into_iter()
440 .flat_map(|c| c.children_named("Selection"))
441 .flat_map(|s| s.children_named("Component"))
442 .chain(
443 components
444 .into_iter()
445 .flat_map(|c| c.children_named("Component"))
446 .filter(|c| c.attr("Selected").and_then(parse_bool) == Some(true)),
447 )
448 .map(read_component)
449 .collect();
450 Ok(Visualization {
451 guid: owned(root.attr("Guid").map(str::trim).filter(|g| !g.is_empty())),
452 selection,
453 })
454}
455
456fn read_component(node: &Node) -> Component {
457 Component {
458 ifc_guid: owned(
459 node.attr("IfcGuid")
460 .map(str::trim)
461 .filter(|g| !g.is_empty()),
462 ),
463 originating_system: owned(node.child_text("OriginatingSystem")),
464 authoring_tool_id: owned(node.child_text("AuthoringToolId")),
465 }
466}
467
468fn read_comment(entry: &str, node: &Node, diagnostics: &mut Vec<Diagnostic>) -> Comment {
469 let guid = owned(node.attr("Guid").map(str::trim).filter(|g| !g.is_empty()));
470 if guid.is_none() {
471 diagnostics.push(Diagnostic::in_entry(entry, Tolerance::CommentWithoutGuid));
472 }
473 Comment {
474 guid,
475 date: owned(node.child_text("Date")),
476 author: owned(node.child_text("Author")),
477 comment: owned(node.child_text("Comment")),
478 viewpoint: node
480 .child("Viewpoint")
481 .and_then(|v| owned(v.attr("Guid")))
482 .or_else(|| owned(node.child_text("Viewpoint"))),
483 modified_date: owned(node.child_text("ModifiedDate")),
484 modified_author: owned(node.child_text("ModifiedAuthor")),
485 }
486}
487
488fn parse_bool(raw: &str) -> Option<bool> {
491 match raw.trim().to_ascii_lowercase().as_str() {
492 "true" | "1" => Some(true),
493 "false" | "0" => Some(false),
494 _ => None,
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::xml::parse;
502
503 fn tree(src: &str) -> Node {
504 parse(src.as_bytes()).unwrap()
505 }
506
507 #[test]
508 fn detects_2_0_from_a_comment_back_reference() {
509 let t = tree(
510 r#"<Markup><Topic Guid="a"/><Comment Guid="c"><Topic Guid="a"/></Comment></Markup>"#,
511 );
512 assert_eq!(observe_version(&t), Some(BcfVersion::V2_0));
513 }
514
515 #[test]
517 fn detects_2_0_from_comment_only_members() {
518 for child in [
519 "<Status>Open</Status>",
520 "<VerbalStatus>Open</VerbalStatus>",
521 r#"<ReplyToComment Guid="x"/>"#,
522 ] {
523 let t = tree(&format!(
524 r#"<Markup><Topic Guid="a" TopicStatus="Open"/><Comment Guid="c">{child}</Comment></Markup>"#
525 ));
526 assert_eq!(observe_version(&t), Some(BcfVersion::V2_0), "for {child}");
527 }
528 }
529
530 #[test]
535 fn attribute_status_is_not_evidence_against_2_0() {
536 let t = tree(
537 r#"<Markup><Topic Guid="a" TopicType="Information" TopicStatus="Open"><Title>T</Title></Topic></Markup>"#,
538 );
539 assert_eq!(observe_version(&t), None);
540 assert_eq!(
542 BcfVersion::reconcile(Some(BcfVersion::V2_0), observe_version(&t)),
543 openbim_core::Detected::Declared(BcfVersion::V2_0)
544 );
545 }
546
547 #[test]
550 fn a_2_0_document_references_element_is_not_a_3_0_wrapper() {
551 let t = tree(
552 r#"<Markup><Topic Guid="a" TopicStatus="Open"><DocumentReferences Guid="d"><ReferencedDocument>a.pdf</ReferencedDocument></DocumentReferences></Topic></Markup>"#,
553 );
554 assert_eq!(observe_version(&t), None);
555 }
556
557 #[test]
558 fn detects_3_0_from_viewpoints_wrapper_or_server_id() {
559 let wrapped = tree(
560 r#"<Markup><Topic Guid="a" TopicStatus="Open"/><Viewpoints><ViewPoint Guid="v"/></Viewpoints></Markup>"#,
561 );
562 assert_eq!(observe_version(&wrapped), Some(BcfVersion::V3_0));
563
564 let server = tree(r#"<Markup><Topic Guid="a" ServerAssignedId="7"/></Markup>"#);
565 assert_eq!(observe_version(&server), Some(BcfVersion::V3_0));
566 }
567
568 #[test]
573 fn a_2_x_viewpoints_element_is_not_a_3_0_wrapper() {
574 let t = tree(
575 r#"<Markup><Topic Guid="a" TopicStatus="Open"/><Viewpoints Guid="v"><Viewpoint>v.bcfv</Viewpoint><Snapshot>s.png</Snapshot></Viewpoints></Markup>"#,
576 );
577 assert_eq!(observe_version(&t), None);
579 }
580
581 #[test]
586 fn detects_3_0_from_collections_nested_inside_topic() {
587 for child in [
588 "<Comments/>",
589 "<Comments><Comment Guid=\"c\"/></Comments>",
590 "<Viewpoints/>",
591 "<Viewpoints><ViewPoint Guid=\"v\"/></Viewpoints>",
592 "<Labels><Label>x</Label></Labels>",
593 "<DocumentReferences><DocumentReference Guid=\"d\"/></DocumentReferences>",
594 "<ReferenceLinks><ReferenceLink>http://x</ReferenceLink></ReferenceLinks>",
595 ] {
596 let t = tree(&format!(
597 r#"<Markup><Topic Guid="a" TopicStatus="OPEN" TopicType="ERROR"><Title>T</Title>{child}</Topic></Markup>"#
598 ));
599 assert_eq!(observe_version(&t), Some(BcfVersion::V3_0), "for {child}");
600 }
601 }
602
603 #[test]
605 fn bare_2_x_labels_are_not_evidence_of_3_0() {
606 let t = tree(
607 r#"<Markup><Topic Guid="a" TopicStatus="Open"><Labels>Architecture</Labels></Topic></Markup>"#,
608 );
609 assert_eq!(observe_version(&t), None);
610 }
611
612 #[test]
613 fn reads_comments_and_viewpoints_nested_in_a_3_0_topic() {
614 let mut d = Vec::new();
615 let m = interpret(
616 "e",
617 &tree(
618 r#"<Markup><Topic Guid="a"><Title>T</Title><Labels><Label>L1</Label><Label>L2</Label></Labels><Comments><Comment Guid="c1"><Comment>hello</Comment></Comment></Comments><Viewpoints><ViewPoint Guid="v1"><Viewpoint>v.bcfv</Viewpoint></ViewPoint></Viewpoints></Topic></Markup>"#,
619 ),
620 &mut d,
621 )
622 .unwrap();
623 assert_eq!(m.comments.len(), 1);
624 assert_eq!(m.comments[0].comment.as_deref(), Some("hello"));
625 assert_eq!(m.viewpoints.len(), 1);
626 assert_eq!(m.viewpoints[0].viewpoint.as_deref(), Some("v.bcfv"));
627 assert_eq!(m.topic.labels, ["L1", "L2"]);
628 assert!(d.is_empty(), "{d:?}");
629 }
630
631 #[test]
633 fn an_empty_viewpoints_wrapper_yields_no_viewpoints() {
634 let mut d = Vec::new();
635 let m = interpret(
636 "e",
637 &tree(r#"<Markup><Topic Guid="a"><Title>T</Title><Viewpoints/><Comments/></Topic></Markup>"#),
638 &mut d,
639 )
640 .unwrap();
641 assert!(m.viewpoints.is_empty(), "{:?}", m.viewpoints);
642 assert!(m.comments.is_empty());
643 }
644
645 #[test]
646 fn indistinguishable_markup_observes_nothing() {
647 let t = tree(r#"<Markup><Topic Guid="a"><Title>x</Title></Topic></Markup>"#);
648 assert_eq!(observe_version(&t), None);
649 assert_eq!(observe_version(&tree("<Markup/>")), None);
650 }
651
652 #[test]
653 fn status_and_type_are_kept_verbatim_not_normalised() {
654 let mut d = Vec::new();
655 let t = tree(
656 r#"<Markup><Topic Guid="a" TopicStatus="Offen" TopicType="formale Prüfung"><Title>T</Title></Topic></Markup>"#,
657 );
658 let m = interpret("x/markup.bcf", &t, &mut d).unwrap();
659 assert_eq!(m.status(), Some("Offen"));
660 assert_eq!(m.topic.topic_type.as_deref(), Some("formale Prüfung"));
661 assert!(d.is_empty(), "{d:?}");
662 }
663
664 #[test]
665 fn missing_guid_and_title_are_diagnosed_but_not_fatal() {
666 let mut d = Vec::new();
667 let m = interpret("x/markup.bcf", &tree("<Markup><Topic/></Markup>"), &mut d).unwrap();
668 assert_eq!(m.topic.guid, None);
669 assert_eq!(m.title(), "");
670 let kinds: Vec<_> = d.iter().map(|x| x.tolerance.clone()).collect();
671 assert!(kinds.contains(&Tolerance::TopicWithoutGuid), "{kinds:?}");
672 assert!(kinds.contains(&Tolerance::TopicWithoutTitle), "{kinds:?}");
673 }
674
675 #[test]
676 fn markup_without_a_topic_yields_nothing_and_says_so() {
677 let mut d = Vec::new();
678 assert!(interpret("x", &tree("<Markup/>"), &mut d).is_none());
679 assert_eq!(d[0].tolerance, Tolerance::MarkupWithoutTopic);
680 }
681
682 #[test]
683 fn reads_both_wrapped_and_unwrapped_viewpoint_references() {
684 let mut d = Vec::new();
685 let v3 = interpret(
686 "e",
687 &tree(
688 r#"<Markup><Topic Guid="a"/><Viewpoints><ViewPoint Guid="v1"><Viewpoint>v1.bcfv</Viewpoint><Snapshot>s1.png</Snapshot><Index>2</Index></ViewPoint></Viewpoints></Markup>"#,
689 ),
690 &mut d,
691 )
692 .unwrap();
693 assert_eq!(v3.viewpoints.len(), 1);
694 assert_eq!(v3.viewpoints[0].guid.as_deref(), Some("v1"));
695 assert_eq!(v3.viewpoints[0].snapshot.as_deref(), Some("s1.png"));
696 assert_eq!(v3.viewpoints[0].index, Some(2));
697
698 let v2 = interpret(
699 "e",
700 &tree(
701 r#"<Markup><Topic Guid="a"/><Viewpoints Guid="v1"><Viewpoint>v1.bcfv</Viewpoint></Viewpoints><Viewpoints Guid="v2"><Viewpoint>v2.bcfv</Viewpoint></Viewpoints></Markup>"#,
702 ),
703 &mut d,
704 )
705 .unwrap();
706 assert_eq!(v2.viewpoints.len(), 2);
707 assert_eq!(v2.viewpoints[1].viewpoint.as_deref(), Some("v2.bcfv"));
708 }
709
710 #[test]
711 fn comments_keep_order_and_anchor_guid() {
712 let mut d = Vec::new();
713 let m = interpret(
714 "e",
715 &tree(
716 r#"<Markup><Topic Guid="a"/><Comment Guid="c1"><Comment>first</Comment><Viewpoint Guid="v1"/></Comment><Comment Guid="c2"><Comment>second</Comment></Comment></Markup>"#,
717 ),
718 &mut d,
719 )
720 .unwrap();
721 assert_eq!(m.comments.len(), 2);
722 assert_eq!(m.comments[0].comment.as_deref(), Some("first"));
723 assert_eq!(m.comments[0].viewpoint.as_deref(), Some("v1"));
724 assert_eq!(m.comments[1].comment.as_deref(), Some("second"));
725 }
726
727 #[test]
728 fn header_files_read_attributes_and_boolean_spellings() {
729 let mut d = Vec::new();
730 let m = interpret(
731 "e",
732 &tree(
733 r#"<Markup><Header><File IfcProject="p1" IsExternal="False"><Filename>a.ifc</Filename><Date>2015-04-14T15:51:25Z</Date></File><File IsExternal="1"><Filename>b.ifc</Filename></File></Header><Topic Guid="a"/></Markup>"#,
734 ),
735 &mut d,
736 )
737 .unwrap();
738 assert_eq!(m.header_files.len(), 2);
739 assert_eq!(m.header_files[0].ifc_project.as_deref(), Some("p1"));
740 assert_eq!(m.header_files[0].is_external, Some(false));
741 assert_eq!(
742 m.header_files[0].date.as_deref(),
743 Some("2015-04-14T15:51:25Z")
744 );
745 assert_eq!(m.header_files[1].is_external, Some(true));
746 }
747
748 #[test]
749 fn related_topics_and_labels_read_in_both_2_x_and_3_0_shapes() {
750 let mut d = Vec::new();
751 let m = interpret(
752 "e",
753 &tree(
754 r#"<Markup><Topic Guid="a"><Labels>L1</Labels><Labels>L2</Labels><RelatedTopic Guid="r1"/><RelatedTopics><RelatedTopic Guid="r2"/></RelatedTopics><ReferenceLinks><ReferenceLink>http://x/1</ReferenceLink></ReferenceLinks></Topic></Markup>"#,
755 ),
756 &mut d,
757 )
758 .unwrap();
759 assert_eq!(m.topic.labels, ["L1", "L2"]);
760 assert_eq!(m.topic.related_topics, ["r1", "r2"]);
761 assert_eq!(m.topic.reference_links, ["http://x/1"]);
762 }
763
764 #[test]
765 fn reads_a_2_1_and_3_0_component_selection() {
766 let v = interpret_visualization(&tree(
767 r#"<VisualizationInfo Guid="v"><Components><Selection><Component IfcGuid="0fXw$sQh19ixbI4tZgfkXu"><OriginatingSystem>Revit</OriginatingSystem><AuthoringToolId>42</AuthoringToolId></Component><Component IfcGuid="1Qj3z6Wtb5WwCLdW5ctWxe"/></Selection><Visibility DefaultVisibility="true"><Exceptions><Component IfcGuid="3cUkl32yn9qRSPvBJVyWYp"/></Exceptions></Visibility></Components></VisualizationInfo>"#,
768 ))
769 .unwrap();
770 assert_eq!(v.guid.as_deref(), Some("v"));
771 assert_eq!(
772 v.selection.len(),
773 2,
774 "visibility exceptions are not a selection"
775 );
776 assert_eq!(
777 v.selection[0],
778 Component {
779 ifc_guid: Some("0fXw$sQh19ixbI4tZgfkXu".into()),
780 originating_system: Some("Revit".into()),
781 authoring_tool_id: Some("42".into()),
782 }
783 );
784 assert_eq!(v.selection[1], Component::ifc("1Qj3z6Wtb5WwCLdW5ctWxe"));
785 }
786
787 #[test]
789 fn reads_a_2_0_selection_from_the_selected_flag() {
790 let v = interpret_visualization(&tree(
791 r#"<VisualizationInfo><Components><Component IfcGuid="0fXw$sQh19ixbI4tZgfkXu" Selected="true"/><Component IfcGuid="1Qj3z6Wtb5WwCLdW5ctWxe" Visible="false"/><Component IfcGuid="3cUkl32yn9qRSPvBJVyWYp" Selected="false"/></Components></VisualizationInfo>"#,
792 ))
793 .unwrap();
794 assert_eq!(v.selection, [Component::ifc("0fXw$sQh19ixbI4tZgfkXu")]);
795 }
796
797 #[test]
798 fn a_non_viewpoint_document_is_refused() {
799 assert!(interpret_visualization(&tree("<Markup/>")).is_err());
800 }
801
802 #[test]
803 fn boolean_parsing_rejects_nonsense_rather_than_defaulting() {
804 assert_eq!(parse_bool("TRUE"), Some(true));
805 assert_eq!(parse_bool(" 0 "), Some(false));
806 assert_eq!(parse_bool("yes"), None);
807 assert_eq!(parse_bool(""), None);
808 }
809}