ifc_model/codec.rs
1//! The serialization seam.
2//!
3//! # Why a trait, and why it lives here
4//!
5//! IFC is a *data model* with several concrete encodings: STEP physical file
6//! (`.ifc`), ifcXML (`.ifcxml`), and prospectively IFC-JSON. They differ only
7//! in syntax — the entity graph is identical.
8//!
9//! Defining [`Codec`] in `ifc-model` means:
10//!
11//! - the data model never depends on any particular encoding;
12//! - a new encoding is a new crate implementing this trait, with no change
13//! here and no change to any consumer;
14//! - conversion between encodings is free — parse with one, write with
15//! another, because both speak [`Model`].
16//!
17//! The inverse layering (a model that depends on the STEP reader) would make
18//! ifcXML support a second parallel stack and make cross-format conversion
19//! lossy.
20
21use crate::error::ModelError;
22use crate::model::Model;
23use std::io::{Read, Write};
24use std::path::Path;
25
26/// Read and write one concrete IFC serialization.
27///
28/// Implementors are stateless; they carry configuration only.
29pub trait Codec {
30 /// Human-readable name for diagnostics, e.g. `STEP`.
31 fn name(&self) -> &'static str;
32
33 /// Conventional file extensions, lower-case, without the dot.
34 fn extensions(&self) -> &'static [&'static str];
35
36 /// Does this look like a file this codec can read?
37 ///
38 /// Content sniffing, so a file with the wrong extension still opens.
39 /// Defaults to `false` — a codec that cannot cheaply recognize its own
40 /// format should say so rather than claim every input, which would make
41 /// codec selection order-dependent.
42 fn detect(&self, _bytes: &[u8]) -> bool {
43 false
44 }
45
46 /// Parse a model from bytes.
47 ///
48 /// Bytes rather than `&str` because IFC files are not guaranteed UTF-8:
49 /// STEP escapes non-ASCII text, and a stray raw byte must not abort the
50 /// parse of an otherwise valid file.
51 fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError>;
52
53 /// Parse a model from a buffer the model may keep.
54 ///
55 /// A codec that loads lazily keeps its source for the model's lifetime;
56 /// handing it the buffer avoids copying the whole input once more.
57 /// Defaults to [`Codec::read_bytes`].
58 fn read_owned(&self, bytes: Vec<u8>) -> Result<Model, ModelError> {
59 self.read_bytes(&bytes)
60 }
61
62 /// Serialize a model.
63 fn write(&self, model: &Model, out: &mut dyn Write) -> Result<(), ModelError>;
64
65 /// Parse from any reader. Override when the format can stream.
66 fn read_from(&self, reader: &mut dyn Read) -> Result<Model, ModelError> {
67 let mut buf = Vec::new();
68 reader
69 .read_to_end(&mut buf)
70 .map_err(|e| ModelError::Io(e.to_string()))?;
71 self.read_owned(buf)
72 }
73
74 /// Parse a file from disk.
75 ///
76 /// Override this when the codec can memory-map instead of reading into a
77 /// heap buffer; `ifc-step` does exactly that for large models.
78 fn read_path(&self, path: &Path) -> Result<Model, ModelError> {
79 let bytes = std::fs::read(path).map_err(|e| ModelError::Io(e.to_string()))?;
80 self.read_bytes(&bytes)
81 }
82
83 /// Serialize to a file on disk.
84 fn write_path(&self, model: &Model, path: &Path) -> Result<(), ModelError> {
85 let mut file = std::fs::File::create(path).map_err(|e| ModelError::Io(e.to_string()))?;
86 self.write(model, &mut file)
87 }
88
89 /// Serialize to a byte vector.
90 fn write_bytes(&self, model: &Model) -> Result<Vec<u8>, ModelError> {
91 let mut out = Vec::new();
92 self.write(model, &mut out)?;
93 Ok(out)
94 }
95}