use std::borrow::Cow;
use std::sync::Arc;
use crate::StepError;
use ifc_model::{Diagnostic, Entity, EntityId, Model, Value};
use openbim_step::{OnMalformed, Parameter, ParseOptions, StandardHeader};
type Text<'a> = Cow<'a, str>;
pub(crate) fn parse(input: &[u8], options: ParseOptions) -> Result<Model, StepError> {
let mut sink = ModelSink {
model: Model::new(),
header: openbim_step::HeaderSection::default(),
recovering: options.on_malformed_record == OnMalformed::Skip,
error: None,
};
let diagnostics = openbim_step::parse_events_borrowed(input, &mut sink, options)?;
if let Some(error) = sink.error {
return Err(error);
}
let mut model = sink.model;
apply_header(model.header_mut(), sink.header.standard());
for diagnostic in &diagnostics {
model.push_diagnostic(Diagnostic::warning(
diagnostic.span().start..diagnostic.span().end,
diagnostic.detail(),
));
}
Ok(model)
}
struct ModelSink {
model: Model,
header: openbim_step::HeaderSection,
recovering: bool,
error: Option<StepError>,
}
impl<'a> openbim_step::EventSink<Text<'a>> for ModelSink {
fn event(&mut self, event: openbim_step::Event<Text<'a>>) {
if self.error.is_some() {
return;
}
match event {
openbim_step::Event::HeaderRecord(record) => {
self.header.records.push(owned_header_record(record));
}
openbim_step::Event::DataRecord(instance) => self.record(instance),
openbim_step::Event::StartHeader
| openbim_step::Event::EndHeader
| openbim_step::Event::StartData
| openbim_step::Event::EndData => {}
}
}
}
impl ModelSink {
fn record(&mut self, instance: openbim_step::DataRecord<Text<'_>>) {
let id_text = instance.id.as_str().to_string();
match convert(instance) {
Ok((id, entity)) => self.model.insert(id, entity),
Err(error) if self.recovering => {
self.model.push_diagnostic(Diagnostic::unlocated(format!(
"skipped unrepresentable record #{id_text}: {error}"
)));
}
Err(error) => self.error = Some(error),
}
}
}
pub(crate) fn convert(
instance: openbim_step::DataRecord<Text<'_>>,
) -> Result<(EntityId, Entity), StepError> {
let id = instance_id(&instance.id)?;
simple(&instance)?;
let record = instance
.records
.into_iter()
.next()
.expect("`simple` guarantees exactly one record");
let attributes = values(record.parameters)?;
Ok((id, Entity::new(upper_arc(record.name), attributes)))
}
pub(crate) fn validate(
instance: &openbim_step::DataRecord<Text<'_>>,
) -> Result<EntityId, StepError> {
let id = instance_id(&instance.id)?;
for parameter in &simple(instance)?.parameters {
check(parameter)?;
}
Ok(id)
}
fn check(parameter: &Parameter<Text<'_>>) -> Result<(), StepError> {
match parameter {
Parameter::Integer(value) => integer(value).map(drop),
Parameter::Real(value) => real(value).map(drop),
Parameter::Ref(id) => reference(id).map(drop),
Parameter::List(items) => items.iter().try_for_each(check),
Parameter::Typed { value, .. } => check(value),
Parameter::Null
| Parameter::Derived
| Parameter::Bool(_)
| Parameter::LogicalUnknown
| Parameter::Text(_)
| Parameter::Binary(_)
| Parameter::Enum(_) => Ok(()),
}
}
fn instance_id(id: &openbim_step::InstanceId) -> Result<EntityId, StepError> {
id.as_str()
.parse()
.map(EntityId)
.map_err(|_| StepError::Syntax {
offset: 0,
detail: "instance id exceeds the IFC record model range".into(),
})
}
fn simple<'r, 'a>(
instance: &'r openbim_step::DataRecord<Text<'a>>,
) -> Result<&'r openbim_step::Record<Text<'a>>, StepError> {
instance.as_simple().ok_or_else(|| StepError::Syntax {
offset: 0,
detail: "complex STEP instances are not representable in the IFC record model".into(),
})
}
fn integer(value: &str) -> Result<i64, StepError> {
value.parse().map_err(|_| StepError::Syntax {
offset: 0,
detail: "integer exceeds the IFC record model range".into(),
})
}
fn real(value: &str) -> Result<f64, StepError> {
value.parse().map_err(|_| StepError::Syntax {
offset: 0,
detail: "real exceeds the IFC record model range".into(),
})
}
fn reference(id: &openbim_step::InstanceId) -> Result<EntityId, StepError> {
id.as_str()
.parse()
.map(EntityId)
.map_err(|_| StepError::Syntax {
offset: 0,
detail: "reference id exceeds the IFC record model range".into(),
})
}
fn upper(name: Text<'_>) -> String {
if name.bytes().any(|byte| byte.is_ascii_lowercase()) {
name.to_ascii_uppercase()
} else {
name.into_owned()
}
}
fn upper_arc(name: Text<'_>) -> Arc<str> {
if name.bytes().any(|byte| byte.is_ascii_lowercase()) {
name.to_ascii_uppercase().into()
} else {
Arc::from(&*name)
}
}
fn owned_header_record(record: openbim_step::HeaderRecord<Text<'_>>) -> openbim_step::HeaderRecord {
openbim_step::HeaderRecord {
name: upper(record.name),
parameters: record.parameters.into_iter().map(owned_parameter).collect(),
}
}
fn owned_parameter(parameter: Parameter<Text<'_>>) -> Parameter {
match parameter {
Parameter::Null => Parameter::Null,
Parameter::Derived => Parameter::Derived,
Parameter::Bool(value) => Parameter::Bool(value),
Parameter::LogicalUnknown => Parameter::LogicalUnknown,
Parameter::Integer(value) => Parameter::Integer(value.into_owned()),
Parameter::Real(value) => Parameter::Real(value.into_owned()),
Parameter::Text(value) => Parameter::Text(value.into_owned()),
Parameter::Binary(value) => Parameter::Binary(value.into_owned()),
Parameter::Enum(value) => Parameter::Enum(upper(value)),
Parameter::Ref(id) => Parameter::Ref(id),
Parameter::List(values) => {
Parameter::List(values.into_iter().map(owned_parameter).collect())
}
Parameter::Typed { type_name, value } => Parameter::Typed {
type_name: upper(type_name),
value: Box::new(owned_parameter(*value)),
},
}
}
pub(crate) fn apply_header(header: &mut ifc_model::header::Header, source: StandardHeader) {
if let Some(value) = source.description {
header.description = value;
}
if let Some(value) = source.implementation_level {
header.implementation_level = value;
}
if let Some(value) = source.name {
header.name = value;
}
if let Some(value) = source.time_stamp {
header.time_stamp = value;
}
if let Some(value) = source.author {
header.author = value;
}
if let Some(value) = source.organization {
header.organization = value;
}
if let Some(value) = source.preprocessor_version {
header.preprocessor_version = value;
}
if let Some(value) = source.originating_system {
header.originating_system = value;
}
if let Some(value) = source.authorization {
header.authorization = value;
}
if let Some(value) = source.schema {
header.schema = value;
}
}
fn values(parameters: Vec<Parameter<Text<'_>>>) -> Result<Vec<Value>, StepError> {
let mut out = Vec::with_capacity(parameters.len());
for parameter in parameters {
out.push(parameter_to_value(parameter)?);
}
Ok(out)
}
fn parameter_to_value(parameter: Parameter<Text<'_>>) -> Result<Value, StepError> {
Ok(match parameter {
Parameter::Null => Value::Null,
Parameter::Derived => Value::Derived,
Parameter::Bool(value) => Value::Bool(value),
Parameter::LogicalUnknown => Value::LogicalUnknown,
Parameter::Integer(value) => Value::Integer(integer(&value)?),
Parameter::Real(value) => Value::Real(real(&value)?),
Parameter::Text(value) => Value::Text(Arc::from(&*value)),
Parameter::Binary(value) => Value::Binary(Arc::from(&*value)),
Parameter::Enum(value) => Value::Enum(upper_arc(value)),
Parameter::Ref(id) => Value::Ref(reference(&id)?),
Parameter::List(items) => Value::List(values(items)?),
Parameter::Typed { type_name, value } => Value::Typed {
type_name: upper_arc(type_name),
value: Box::new(parameter_to_value(*value)?),
},
})
}