1use serde_json::{json, Map, Value};
7
8use crate::model::{build_export_model, EntityRow, PropValue};
9
10pub struct JsonOptions {
12 pub pretty: bool,
14 pub include_properties: bool,
16 pub include_quantities: bool,
18}
19
20impl Default for JsonOptions {
21 fn default() -> Self {
22 Self { pretty: false, include_properties: true, include_quantities: true }
23 }
24}
25
26pub(crate) fn finite_json_number(n: f64) -> Value {
36 if n.is_finite() { json!(n) } else { json!(n.to_string()) }
37}
38
39pub(crate) fn typed_value(p: &PropValue) -> Value {
41 match p.value_type.as_str() {
42 "IFCREAL" | "IFCINTEGER" | "IFCNUMBER" | "IFCLENGTHMEASURE" | "IFCAREAMEASURE"
43 | "IFCVOLUMEMEASURE" | "IFCPOSITIVELENGTHMEASURE" | "IFCCOUNTMEASURE"
44 | "IFCMASSMEASURE" | "IFCRATIOMEASURE" | "IFCNORMALISEDRATIOMEASURE" => {
45 p.value.parse::<f64>().map(finite_json_number).unwrap_or_else(|_| json!(p.value))
46 }
47 "IFCBOOLEAN" | "IFCLOGICAL" => match p.value.as_str() {
48 "true" => json!(true),
49 "false" => json!(false),
50 other => json!(other),
51 },
52 _ => json!(p.value),
53 }
54}
55
56fn entity_to_json(e: &EntityRow, opts: &JsonOptions) -> Value {
57 let mut obj = Map::new();
58 obj.insert("expressId".into(), json!(e.express_id));
59 obj.insert("type".into(), json!(e.ifc_type));
60 if let Some(g) = &e.global_id {
61 obj.insert("globalId".into(), json!(g));
62 }
63 if let Some(n) = &e.name {
64 obj.insert("name".into(), json!(n));
65 }
66 if let Some(d) = &e.description {
67 obj.insert("description".into(), json!(d));
68 }
69 if let Some(o) = &e.object_type {
70 obj.insert("objectType".into(), json!(o));
71 }
72 obj.insert("hasGeometry".into(), json!(e.has_geometry));
73
74 if opts.include_properties && !e.property_sets.is_empty() {
75 let psets: Vec<Value> = e
76 .property_sets
77 .iter()
78 .map(|ps| {
79 let props: Vec<Value> = ps
80 .properties
81 .iter()
82 .map(|p| json!({ "name": p.name, "value": typed_value(p), "type": p.value_type }))
83 .collect();
84 json!({ "name": ps.name, "properties": props })
85 })
86 .collect();
87 obj.insert("propertySets".into(), json!(psets));
88 }
89
90 if opts.include_quantities && !e.quantity_sets.is_empty() {
91 let qsets: Vec<Value> = e
92 .quantity_sets
93 .iter()
94 .map(|qs| {
95 let quants: Vec<Value> = qs
96 .quantities
97 .iter()
98 .map(|q| json!({ "name": q.name, "value": finite_json_number(q.value), "type": q.kind }))
99 .collect();
100 json!({ "name": qs.name, "quantities": quants })
101 })
102 .collect();
103 obj.insert("quantitySets".into(), json!(qsets));
104 }
105
106 Value::Object(obj)
107}
108
109pub fn export_json(content: &[u8], opts: &JsonOptions) -> String {
111 let model = build_export_model(content);
112 let arr: Vec<Value> = model.entities.iter().map(|e| entity_to_json(e, opts)).collect();
113 let value = Value::Array(arr);
114 if opts.pretty {
115 serde_json::to_string_pretty(&value).expect("json serializes")
116 } else {
117 serde_json::to_string(&value).expect("json serializes")
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn duplex_exports_json_array() {
127 let s = export_json(&fixture_or_skip!("ara3d/duplex.ifc"), &JsonOptions::default());
128 let v: Value = serde_json::from_str(&s).expect("valid JSON");
129 let arr = v.as_array().expect("array");
130 assert!(arr.len() > 50);
131 let first = &arr[0];
132 assert!(first["expressId"].is_number());
133 assert!(first["type"].is_string());
134 assert!(first["hasGeometry"].is_boolean());
135
136 let has_typed_number = arr.iter().any(|e| {
138 e["propertySets"].as_array().is_some_and(|ps| {
139 ps.iter().any(|p| {
140 p["properties"].as_array().is_some_and(|props| {
141 props.iter().any(|pr| pr["value"].is_number() || pr["value"].is_boolean())
142 })
143 })
144 })
145 });
146 assert!(has_typed_number, "expected at least one typed (number/bool) property value");
147 }
148
149 #[test]
157 fn a_non_finite_quantity_survives_instead_of_becoming_null() {
158 let ifc = "ISO-10303-21;\n\
159HEADER;\n\
160FILE_DESCRIPTION((''),'');\n\
161FILE_NAME('','',(''),(''),'','','');\n\
162FILE_SCHEMA(('IFC4'));\n\
163ENDSEC;\n\
164DATA;\n\
165#1=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n\
166#3=IFCUNITASSIGNMENT((#1));\n\
167#4=IFCPROJECT('0PROJECT0000000000000',$,'P',$,$,$,$,$,#3);\n\
168#5=IFCWALL('0WALL000000000000000A',$,'W',$,$,$,$,$,$);\n\
169#20=IFCQUANTITYLENGTH('Length',$,$,1.0E400);\n\
170#21=IFCELEMENTQUANTITY('0QTO00000000000000A',$,'Qto_WallBaseQuantities',$,$,(#20));\n\
171#22=IFCRELDEFINESBYPROPERTIES('0REL000000000000000A',$,$,$,(#5),#21);\n\
172ENDSEC;\n\
173END-ISO-10303-21;\n";
174 let s = export_json(ifc.as_bytes(), &JsonOptions::default());
175 let v: Value = serde_json::from_str(&s).expect("valid JSON");
176 let quantities = v[0]["quantitySets"][0]["quantities"].as_array().expect("quantities array");
177 let length = quantities.iter().find(|q| q["name"] == "Length").expect("Length quantity present");
178 assert!(!length["value"].is_null(), "an out-of-range REAL must not collapse to null");
179 assert_eq!(length["value"], json!("inf"));
180 }
181}