openbim_ifcx_binding_core/
document.rs1use openbim_ifcx::IfcxFile;
4
5use crate::error::BindingError;
6use crate::report;
7
8#[derive(Debug, Clone, PartialEq)]
14pub struct Document {
15 file: IfcxFile,
16}
17
18impl Document {
19 pub fn parse(bytes: &[u8]) -> Result<Self, BindingError> {
21 IfcxFile::from_json_slice(bytes)
22 .map(|file| Self { file })
23 .map_err(|e| BindingError::Read(e.to_string()))
24 }
25
26 pub fn write(&self, pretty: bool) -> Result<String, BindingError> {
28 let written = if pretty {
29 self.file.to_json_string_pretty()
30 } else {
31 self.file.to_json_string()
32 };
33 written.map_err(|e| BindingError::Write(e.to_string()))
34 }
35
36 pub fn header_json(&self) -> Result<String, BindingError> {
38 serde_json::to_string(&self.file.header).map_err(|e| BindingError::Write(e.to_string()))
39 }
40
41 pub fn node_count(&self) -> usize {
43 self.file.data.len()
44 }
45
46 pub fn validate_json(&self) -> String {
53 report::to_json(&self.file.validate().err().unwrap_or_default())
54 }
55
56 pub fn file(&self) -> &IfcxFile {
58 &self.file
59 }
60
61 pub fn into_file(self) -> IfcxFile {
63 self.file
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use serde_json::Value;
70
71 use super::Document;
72 use crate::fixtures::fixture;
73
74 #[test]
75 fn every_fixture_round_trips_through_write() {
76 for bytes in [
77 fixture!("minimal.ifcx"),
78 fixture!("wall-with-type.ifcx"),
79 fixture!("unknown-fields.ifcx"),
80 fixture!("geometry-model.ifcx"),
81 ] {
82 let document = Document::parse(bytes).unwrap();
83 let again = Document::parse(document.write(false).unwrap().as_bytes()).unwrap();
84 assert_eq!(again, document);
85 let pretty = Document::parse(document.write(true).unwrap().as_bytes()).unwrap();
86 assert_eq!(pretty, document);
87 }
88 }
89
90 #[test]
91 fn unknown_fields_and_exact_numbers_survive() {
92 let bytes = fixture!("unknown-fields.ifcx");
93 let written = Document::parse(bytes).unwrap().write(false).unwrap();
94 let original: Value = serde_json::from_slice(bytes).unwrap();
95 let written: Value = serde_json::from_str(&written).unwrap();
96 assert_eq!(written, original);
97 }
98
99 #[test]
100 fn header_is_plain_json() {
101 let document = Document::parse(fixture!("minimal.ifcx")).unwrap();
102 let header: Value = serde_json::from_str(&document.header_json().unwrap()).unwrap();
103 assert_eq!(header["ifcxVersion"], "ifcx_alpha");
104 assert_eq!(document.node_count(), 0);
105 }
106
107 #[test]
108 fn invalid_input_is_a_read_error_with_a_location() {
109 let error = Document::parse(b"{\n \"header\": 1\n}").unwrap_err();
110 assert_eq!(error.code(), "read");
111 assert!(error.to_string().contains("line 2"), "{error}");
112 assert_eq!(Document::parse(b"not json").unwrap_err().code(), "read");
113 assert_eq!(Document::parse(&[0xff, 0xfe]).unwrap_err().code(), "read");
114 }
115
116 #[test]
117 fn validation_reports_every_failure_as_data() {
118 let valid = Document::parse(fixture!("valid-attributes.ifcx")).unwrap();
119 let report: Value = serde_json::from_str(&valid.validate_json()).unwrap();
120 assert_eq!(report["valid"], true);
121 assert_eq!(report["failures"].as_array().unwrap().len(), 0);
122
123 let invalid = Document::parse(fixture!("invalid-attributes.ifcx")).unwrap();
124 let report: Value = serde_json::from_str(&invalid.validate_json()).unwrap();
125 assert_eq!(report["valid"], false);
126 let failures = report["failures"].as_array().unwrap();
127 let expected = invalid.file().validate().unwrap_err().failures;
128 assert_eq!(failures.len(), expected.len());
129 for (json, failure) in failures.iter().zip(&expected) {
130 assert_eq!(json["node"], failure.node.as_str());
131 assert_eq!(json["attribute"], failure.attribute.as_str());
132 assert_eq!(json["pointer"], failure.pointer.as_str());
133 assert_eq!(json["message"], failure.kind.to_string());
134 assert_ne!(json["kind"], "other", "{failure}");
135 }
136 }
137}