1use core::fmt;
36
37use openbim_core::Detected;
38
39use crate::model::{
40 Applicability, Entity, Facet, Info, Requirement, Requirements, Restriction, Specification,
41 Value,
42};
43use crate::read::{is_author, is_xs_date, XSD_NAMESPACE, XSD_TYPES};
44use crate::{Ids, IdsVersion, Occurrence, NAMESPACE};
45
46const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
47
48pub const SCHEMA_LOCATION: &str = "http://standards.buildingsmart.org/IDS/1.0/ids.xsd";
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct WriteError {
54 kind: WriteErrorKind,
55 location: String,
56}
57
58impl WriteError {
59 #[must_use]
61 pub fn kind(&self) -> &WriteErrorKind {
62 &self.kind
63 }
64
65 #[must_use]
69 pub fn location(&self) -> &str {
70 &self.location
71 }
72}
73
74impl fmt::Display for WriteError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 if self.location.is_empty() {
77 write!(f, "{}", self.kind)
78 } else {
79 write!(f, "{}: {}", self.location, self.kind)
80 }
81 }
82}
83
84impl std::error::Error for WriteError {}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum WriteErrorKind {
90 UnsupportedVersion(Detected<IdsVersion>),
92 NoSpecifications,
94 NoIfcVersions,
96 EmptyApplicability,
98 ApplicabilityOrder {
102 found: &'static str,
104 },
105 AttributeNotAllowed {
107 facet: &'static str,
109 attribute: &'static str,
111 },
112 OccurrenceNotAllowed {
115 facet: &'static str,
117 occurrence: Occurrence,
119 },
120 EmptyRestriction,
122 InvalidValue {
124 found: String,
126 expected: &'static str,
128 },
129 InvalidCharacter(char),
131 Io {
133 kind: std::io::ErrorKind,
135 message: String,
137 },
138}
139
140impl fmt::Display for WriteErrorKind {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 match self {
143 WriteErrorKind::UnsupportedVersion(detected) => match detected {
144 Detected::Conflict { declared, observed } => write!(
145 f,
146 "the version is a conflict (declared {declared}, observed {observed}); only 1.0 is written"
147 ),
148 Detected::Declared(version) | Detected::Inferred(version) => {
149 write!(f, "the model is IDS {version}; only 1.0 is written")
150 }
151 },
152 WriteErrorKind::NoSpecifications => {
153 f.write_str("IDS 1.0 requires at least one specification")
154 }
155 WriteErrorKind::NoIfcVersions => {
156 f.write_str("a specification must list at least one IFC release")
157 }
158 WriteErrorKind::EmptyApplicability => {
159 f.write_str("an applicability without facets matches every object")
160 }
161 WriteErrorKind::ApplicabilityOrder { found } => write!(
162 f,
163 "<{found}> is out of order; applicability takes at most one entity, then partOf, \
164 classification, attribute, property and material facets"
165 ),
166 WriteErrorKind::AttributeNotAllowed { facet, attribute } => {
167 write!(f, "a <{facet}> requirement cannot carry @{attribute}")
168 }
169 WriteErrorKind::OccurrenceNotAllowed { facet, occurrence } => {
170 write!(f, "a <{facet}> requirement cannot be {occurrence}")
171 }
172 WriteErrorKind::EmptyRestriction => {
173 f.write_str("an xs:restriction without facets constrains nothing")
174 }
175 WriteErrorKind::InvalidValue { found, expected } => {
176 write!(f, "{found:?} is not {expected}")
177 }
178 WriteErrorKind::InvalidCharacter(c) => {
179 write!(f, "U+{:04X} cannot be represented in XML 1.0", u32::from(*c))
180 }
181 WriteErrorKind::Io { message, .. } => write!(f, "writing failed: {message}"),
182 }
183 }
184}
185
186pub fn to_string(ids: &Ids) -> Result<String, WriteError> {
209 let mut writer = Writer::default();
210 writer.ids(ids)?;
211 Ok(writer.out)
212}
213
214pub fn to_writer(ids: &Ids, mut output: impl std::io::Write) -> Result<(), WriteError> {
224 let text = to_string(ids)?;
225 output
226 .write_all(text.as_bytes())
227 .and_then(|()| output.flush())
228 .map_err(|error| WriteError {
229 kind: WriteErrorKind::Io {
230 kind: error.kind(),
231 message: error.to_string(),
232 },
233 location: String::new(),
234 })
235}
236
237type Result<T, E = WriteError> = core::result::Result<T, E>;
238
239#[derive(Default)]
240struct Writer {
241 out: String,
242 depth: usize,
243 path: Vec<String>,
244}
245
246type Attr<'a> = (&'static str, Option<&'a str>);
248
249impl Writer {
250 fn error(&self, kind: WriteErrorKind) -> WriteError {
251 WriteError {
252 kind,
253 location: self.path.join("/"),
254 }
255 }
256
257 fn error_at(&self, step: &str, kind: WriteErrorKind) -> WriteError {
258 let mut error = self.error(kind);
259 if !error.location.is_empty() {
260 error.location.push('/');
261 }
262 error.location.push_str(step);
263 error
264 }
265
266 fn at<T>(&mut self, step: String, f: impl FnOnce(&mut Self) -> Result<T>) -> Result<T> {
267 self.path.push(step);
268 let result = f(self)?;
269 self.path.pop();
270 Ok(result)
271 }
272
273 fn indent(&mut self) {
274 for _ in 0..self.depth {
275 self.out.push_str(" ");
276 }
277 }
278
279 fn open_tag(&mut self, name: &str, attributes: &[Attr<'_>]) -> Result<()> {
280 self.indent();
281 self.out.push('<');
282 self.out.push_str(name);
283 for (attribute, value) in attributes {
284 if let Some(value) = value {
285 self.path.push(format!("@{attribute}"));
286 check_chars(value).map_err(|c| self.error(WriteErrorKind::InvalidCharacter(c)))?;
287 self.path.pop();
288 self.out.push(' ');
289 self.out.push_str(attribute);
290 self.out.push_str("=\"");
291 escape(&mut self.out, value, true);
292 self.out.push('"');
293 }
294 }
295 Ok(())
296 }
297
298 fn element(
300 &mut self,
301 name: &str,
302 attributes: &[Attr<'_>],
303 f: impl FnOnce(&mut Self) -> Result<()>,
304 ) -> Result<()> {
305 self.open_tag(name, attributes)?;
306 self.out.push_str(">\n");
307 self.depth += 1;
308 f(self)?;
309 self.depth -= 1;
310 self.indent();
311 self.out.push_str("</");
312 self.out.push_str(name);
313 self.out.push_str(">\n");
314 Ok(())
315 }
316
317 fn empty(&mut self, name: &str, attributes: &[Attr<'_>]) -> Result<()> {
318 self.open_tag(name, attributes)?;
319 self.out.push_str("/>\n");
320 Ok(())
321 }
322
323 fn text(&mut self, name: &'static str, text: &str) -> Result<()> {
324 self.at(name.to_owned(), |w| {
325 check_chars(text).map_err(|c| w.error(WriteErrorKind::InvalidCharacter(c)))?;
326 w.open_tag(name, &[])?;
327 w.out.push('>');
328 escape(&mut w.out, text, false);
329 w.out.push_str("</");
330 w.out.push_str(name);
331 w.out.push_str(">\n");
332 Ok(())
333 })
334 }
335
336 fn optional_text(&mut self, name: &'static str, text: Option<&str>) -> Result<()> {
337 text.map_or(Ok(()), |text| self.text(name, text))
338 }
339
340 fn ids(&mut self, ids: &Ids) -> Result<()> {
341 if ids.version.resolved() != Some(IdsVersion::Ids1_0) {
342 return Err(self.error(WriteErrorKind::UnsupportedVersion(ids.version.clone())));
343 }
344 if ids.specifications.is_empty() {
345 return Err(self.error(WriteErrorKind::NoSpecifications));
346 }
347 self.out
348 .push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
349 let location = format!("{NAMESPACE} {SCHEMA_LOCATION}");
350 self.element(
351 "ids",
352 &[
353 ("xmlns", Some(NAMESPACE)),
354 ("xmlns:xs", Some(XSD_NAMESPACE)),
355 ("xmlns:xsi", Some(XSI_NAMESPACE)),
356 ("xsi:schemaLocation", Some(&location)),
357 ],
358 |w| {
359 w.at("info".into(), |w| w.info(&ids.info))?;
360 w.element("specifications", &[], |w| {
361 for (index, specification) in ids.specifications.iter().enumerate() {
362 w.at(format!("specifications[{index}]"), |w| {
363 w.specification(specification)
364 })?;
365 }
366 Ok(())
367 })
368 },
369 )
370 }
371
372 fn info(&mut self, info: &Info) -> Result<()> {
373 if let Some(author) = &info.author {
374 if !is_author(author) {
375 return Err(self.error_at(
376 "author",
377 WriteErrorKind::InvalidValue {
378 found: author.clone(),
379 expected: "an e-mail address (pattern [^@]+@[^\\.]+\\..+)",
380 },
381 ));
382 }
383 }
384 if let Some(date) = &info.date {
385 if !is_xs_date(date) {
386 return Err(self.error_at(
387 "date",
388 WriteErrorKind::InvalidValue {
389 found: date.clone(),
390 expected: "an xs:date such as 2024-06-01",
391 },
392 ));
393 }
394 }
395 self.element("info", &[], |w| {
396 w.text("title", &info.title)?;
397 w.optional_text("copyright", info.copyright.as_deref())?;
398 w.optional_text("version", info.version.as_deref())?;
399 w.optional_text("description", info.description.as_deref())?;
400 w.optional_text("author", info.author.as_deref())?;
401 w.optional_text("date", info.date.as_deref())?;
402 w.optional_text("purpose", info.purpose.as_deref())?;
403 w.optional_text("milestone", info.milestone.as_deref())
404 })
405 }
406
407 fn specification(&mut self, specification: &Specification) -> Result<()> {
408 if specification.ifc_versions.is_empty() {
409 return Err(self.error(WriteErrorKind::NoIfcVersions));
410 }
411 let versions = specification
412 .ifc_versions
413 .iter()
414 .map(|v| v.as_str())
415 .collect::<Vec<_>>()
416 .join(" ");
417 self.element(
418 "specification",
419 &[
420 ("name", Some(&specification.name)),
421 ("ifcVersion", Some(&versions)),
422 ("identifier", specification.identifier.as_deref()),
423 ("description", specification.description.as_deref()),
424 ("instructions", specification.instructions.as_deref()),
425 ],
426 |w| {
427 w.at("applicability".into(), |w| {
428 w.applicability(&specification.applicability)
429 })?;
430 if let Some(requirements) = &specification.requirements {
431 w.at("requirements".into(), |w| w.requirements(requirements))?;
432 }
433 Ok(())
434 },
435 )
436 }
437
438 fn applicability(&mut self, applicability: &Applicability) -> Result<()> {
439 if applicability.facets.is_empty() {
440 return Err(self.error(WriteErrorKind::EmptyApplicability));
441 }
442 let mut rank = 0;
443 for (index, facet) in applicability.facets.iter().enumerate() {
444 let this = applicability_rank(facet);
445 if this < rank || (index > 0 && this == 0) {
447 return Err(self.error_at(
448 &format!("facets[{index}]"),
449 WriteErrorKind::ApplicabilityOrder {
450 found: facet.kind(),
451 },
452 ));
453 }
454 rank = this;
455 }
456 let min = (applicability.min_occurs != 1).then(|| applicability.min_occurs.to_string());
457 let max = match applicability.max_occurs {
458 Some(1) => None,
459 Some(max) => Some(max.to_string()),
460 None => Some("unbounded".to_owned()),
461 };
462 self.element(
463 "applicability",
464 &[("minOccurs", min.as_deref()), ("maxOccurs", max.as_deref())],
465 |w| {
466 for (index, facet) in applicability.facets.iter().enumerate() {
467 w.at(format!("facets[{index}]"), |w| w.facet(facet, None))?;
468 }
469 Ok(())
470 },
471 )
472 }
473
474 fn requirements(&mut self, requirements: &Requirements) -> Result<()> {
475 let attributes = [("description", requirements.description.as_deref())];
476 if requirements.facets.is_empty() {
477 return self.empty("requirements", &attributes);
478 }
479 self.element("requirements", &attributes, |w| {
480 for (index, requirement) in requirements.facets.iter().enumerate() {
481 w.at(format!("facets[{index}]"), |w| {
482 w.facet(&requirement.facet, Some(requirement))
483 })?;
484 }
485 Ok(())
486 })
487 }
488
489 fn facet(&mut self, facet: &Facet, requirement: Option<&Requirement>) -> Result<()> {
491 let kind = facet.kind();
492 let mut cardinality = None;
493 let mut uri = None;
494 let mut instructions = None;
495 if let Some(requirement) = requirement {
496 let occurrence = requirement.occurrence;
497 let refused = match facet {
498 Facet::Entity(_) => occurrence != Occurrence::Required,
499 Facet::PartOf(_) => occurrence == Occurrence::Optional,
500 _ => false,
501 };
502 if refused {
503 return Err(self.error(WriteErrorKind::OccurrenceNotAllowed {
504 facet: kind,
505 occurrence,
506 }));
507 }
508 if !matches!(facet, Facet::Entity(_)) {
509 cardinality = Some(occurrence.as_cardinality());
510 }
511 if requirement.uri.is_some() {
512 if !matches!(
513 facet,
514 Facet::Classification(_) | Facet::Property(_) | Facet::Material(_)
515 ) {
516 return Err(self.error(WriteErrorKind::AttributeNotAllowed {
517 facet: kind,
518 attribute: "uri",
519 }));
520 }
521 uri = requirement.uri.as_deref();
522 }
523 instructions = requirement.instructions.as_deref();
524 }
525 match facet {
526 Facet::Entity(entity) => {
527 self.element("entity", &[("instructions", instructions)], |w| {
528 w.entity_content(entity)
529 })
530 }
531 Facet::PartOf(part_of) => self.element(
532 "partOf",
533 &[
534 ("relation", part_of.relation.map(|r| r.as_str())),
535 ("cardinality", cardinality),
536 ("instructions", instructions),
537 ],
538 |w| {
539 w.at("entity".into(), |w| {
540 w.element("entity", &[], |w| w.entity_content(&part_of.entity))
541 })
542 },
543 ),
544 Facet::Classification(classification) => self.element(
545 "classification",
546 &[
547 ("uri", uri),
548 ("cardinality", cardinality),
549 ("instructions", instructions),
550 ],
551 |w| {
552 w.optional_value("value", classification.value.as_ref())?;
553 w.value("system", &classification.system)
554 },
555 ),
556 Facet::Attribute(attribute) => self.element(
557 "attribute",
558 &[("cardinality", cardinality), ("instructions", instructions)],
559 |w| {
560 w.value("name", &attribute.name)?;
561 w.optional_value("value", attribute.value.as_ref())
562 },
563 ),
564 Facet::Property(property) => {
565 if let Some(data_type) = &property.data_type {
566 if data_type.is_empty() || !data_type.bytes().all(|b| b.is_ascii_uppercase()) {
567 return Err(self.error_at(
568 "@dataType",
569 WriteErrorKind::InvalidValue {
570 found: data_type.clone(),
571 expected: "an upper-case name matching [A-Z]+",
572 },
573 ));
574 }
575 }
576 self.element(
577 "property",
578 &[
579 ("dataType", property.data_type.as_deref()),
580 ("uri", uri),
581 ("cardinality", cardinality),
582 ("instructions", instructions),
583 ],
584 |w| {
585 w.value("propertySet", &property.property_set)?;
586 w.value("baseName", &property.base_name)?;
587 w.optional_value("value", property.value.as_ref())
588 },
589 )
590 }
591 Facet::Material(material) => {
592 let attributes = [
593 ("uri", uri),
594 ("cardinality", cardinality),
595 ("instructions", instructions),
596 ];
597 match &material.value {
598 Some(value) => {
599 self.element("material", &attributes, |w| w.value("value", value))
600 }
601 None => self.empty("material", &attributes),
602 }
603 }
604 }
605 }
606
607 fn entity_content(&mut self, entity: &Entity) -> Result<()> {
608 self.value("name", &entity.name)?;
609 self.optional_value("predefinedType", entity.predefined_type.as_ref())
610 }
611
612 fn optional_value(&mut self, name: &'static str, value: Option<&Value>) -> Result<()> {
613 value.map_or(Ok(()), |value| self.value(name, value))
614 }
615
616 fn value(&mut self, name: &'static str, value: &Value) -> Result<()> {
617 self.at(name.to_owned(), |w| {
618 w.element(name, &[], |w| match value {
619 Value::Simple(text) => w.text("simpleValue", text),
620 Value::Restriction(restriction) => {
621 w.at("xs:restriction".into(), |w| w.restriction(restriction))
622 }
623 })
624 })
625 }
626
627 fn restriction(&mut self, restriction: &Restriction) -> Result<()> {
628 if !XSD_TYPES.contains(&restriction.base.as_str()) {
629 return Err(self.error_at(
630 "@base",
631 WriteErrorKind::InvalidValue {
632 found: restriction.base.clone(),
633 expected:
634 "the local name of an XML Schema built-in simple type, such as string",
635 },
636 ));
637 }
638 if restriction.total_digits == Some(0) {
639 return Err(self.error_at(
640 "xs:totalDigits",
641 WriteErrorKind::InvalidValue {
642 found: "0".into(),
643 expected: "a positive integer",
644 },
645 ));
646 }
647 let counts = [
648 ("xs:length", restriction.length),
649 ("xs:minLength", restriction.min_length),
650 ("xs:maxLength", restriction.max_length),
651 ("xs:totalDigits", restriction.total_digits),
652 ("xs:fractionDigits", restriction.fraction_digits),
653 ];
654 let bounds = [
655 ("xs:minInclusive", restriction.min_inclusive.as_deref()),
656 ("xs:maxInclusive", restriction.max_inclusive.as_deref()),
657 ("xs:minExclusive", restriction.min_exclusive.as_deref()),
658 ("xs:maxExclusive", restriction.max_exclusive.as_deref()),
659 ];
660 let has_facets = !restriction.enumeration.is_empty()
661 || !restriction.patterns.is_empty()
662 || counts.iter().any(|(_, v)| v.is_some())
663 || bounds.iter().any(|(_, v)| v.is_some());
664 if !has_facets {
665 return Err(self.error(WriteErrorKind::EmptyRestriction));
666 }
667 let base = format!("xs:{}", restriction.base);
668 self.element("xs:restriction", &[("base", Some(&base))], |w| {
669 for value in &restriction.enumeration {
670 w.at("xs:enumeration".into(), |w| {
671 w.empty("xs:enumeration", &[("value", Some(value))])
672 })?;
673 }
674 for value in &restriction.patterns {
675 w.at("xs:pattern".into(), |w| {
676 w.empty("xs:pattern", &[("value", Some(value))])
677 })?;
678 }
679 for (name, value) in bounds {
680 if let Some(value) = value {
681 w.at(name.into(), |w| w.empty(name, &[("value", Some(value))]))?;
682 }
683 }
684 for (name, value) in counts {
685 if let Some(value) = value {
686 w.empty(name, &[("value", Some(&value.to_string()))])?;
687 }
688 }
689 Ok(())
690 })
691 }
692}
693
694fn applicability_rank(facet: &Facet) -> u8 {
696 match facet {
697 Facet::Entity(_) => 0,
698 Facet::PartOf(_) => 1,
699 Facet::Classification(_) => 2,
700 Facet::Attribute(_) => 3,
701 Facet::Property(_) => 4,
702 Facet::Material(_) => 5,
703 }
704}
705
706fn check_chars(text: &str) -> Result<(), char> {
708 match text.chars().find(|&c| {
709 !matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..)
710 }) {
711 Some(c) => Err(c),
712 None => Ok(()),
713 }
714}
715
716fn escape(out: &mut String, text: &str, attribute: bool) {
722 for c in text.chars() {
723 match c {
724 '&' => out.push_str("&"),
725 '<' => out.push_str("<"),
726 '>' => out.push_str(">"),
727 '\r' => out.push_str(" "),
728 '"' if attribute => out.push_str("""),
729 '\t' if attribute => out.push_str("	"),
730 '\n' if attribute => out.push_str(" "),
731 c => out.push(c),
732 }
733 }
734}