use super::level::normalise_field_name;
use crate::formats;
use crate::walker::{DecodedElement, ElementDecoder, HandleIndex, InstanceField};
use crate::{Error, Result};
macro_rules! simple_decoder {
($Struct:ident, $name:literal) => {
pub struct $Struct;
impl ElementDecoder for $Struct {
fn class_name(&self) -> &'static str {
$name
}
fn decode(
&self,
bytes: &[u8],
schema: &formats::ClassEntry,
_index: &HandleIndex,
) -> Result<DecodedElement> {
if schema.name != $name {
return Err(Error::BasicFileInfo(format!(
"{} received wrong schema: {}",
stringify!($Struct),
schema.name
)));
}
Ok(crate::walker::decode_instance(bytes, 0, schema))
}
}
};
}
simple_decoder!(PhaseDecoder, "Phase");
simple_decoder!(DesignOptionDecoder, "DesignOption");
simple_decoder!(WorksetDecoder, "Workset");
simple_decoder!(RevisionDecoder, "Revision");
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Phase {
pub name: Option<String>,
pub description: Option<String>,
pub sequence_number: Option<u32>,
}
impl Phase {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self::default();
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
("name", InstanceField::String(s)) => out.name = Some(s.clone()),
("description", InstanceField::String(s)) => {
out.description = Some(s.clone());
}
("sequencenumber" | "sequence", InstanceField::Integer { value, .. }) => {
out.sequence_number = Some(*value as u32);
}
_ => {}
}
}
out
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct DesignOption {
pub name: Option<String>,
pub is_primary: Option<bool>,
pub option_set_id: Option<u32>,
}
impl DesignOption {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self::default();
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
("name", InstanceField::String(s)) => out.name = Some(s.clone()),
("isprimary" | "primary", InstanceField::Bool(b)) => {
out.is_primary = Some(*b);
}
("optionsetid" | "setid", InstanceField::ElementId { id, .. }) => {
out.option_set_id = Some(*id);
}
_ => {}
}
}
out
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Workset {
pub name: Option<String>,
pub unique_id: Option<String>,
pub is_open: Option<bool>,
pub is_editable: Option<bool>,
}
impl Workset {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self::default();
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
("name", InstanceField::String(s)) => out.name = Some(s.clone()),
("uniqueid" | "guid", InstanceField::String(s)) => {
out.unique_id = Some(s.clone());
}
("isopen", InstanceField::Bool(b)) => out.is_open = Some(*b),
("iseditable" | "editable", InstanceField::Bool(b)) => {
out.is_editable = Some(*b);
}
_ => {}
}
}
out
}
pub fn is_modifiable(&self) -> Option<bool> {
Some(self.is_open? && self.is_editable?)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RevisionStatus {
#[default]
Draft,
Issued,
Other,
}
impl RevisionStatus {
pub fn from_code(code: u32) -> Self {
match code {
0 => Self::Draft,
1 => Self::Issued,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Revision {
pub sequence_number: Option<u32>,
pub revision_date: Option<String>,
pub description: Option<String>,
pub issued_to: Option<String>,
pub issued_by: Option<String>,
pub status: Option<RevisionStatus>,
}
impl Revision {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self::default();
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
(
"sequencenumber" | "sequence" | "number",
InstanceField::Integer { value, .. },
) => {
out.sequence_number = Some(*value as u32);
}
("revisiondate" | "date", InstanceField::String(s)) => {
out.revision_date = Some(s.clone());
}
("description", InstanceField::String(s)) => {
out.description = Some(s.clone());
}
("issuedto", InstanceField::String(s)) => {
out.issued_to = Some(s.clone());
}
("issuedby", InstanceField::String(s)) => {
out.issued_by = Some(s.clone());
}
("status" | "revisionstatus", InstanceField::Integer { value, .. }) => {
out.status = Some(RevisionStatus::from_code(*value as u32));
}
_ => {}
}
}
out
}
pub fn is_issued(&self) -> bool {
matches!(self.status, Some(RevisionStatus::Issued))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phase_rejects_wrong_schema() {
use crate::formats::ClassEntry;
let wrong = ClassEntry {
name: "Wall".into(),
offset: 0,
fields: vec![],
tag: None,
parent: None,
declared_field_count: None,
was_parent_only: false,
ancestor_tag: None,
};
assert!(
PhaseDecoder
.decode(&[], &wrong, &HandleIndex::new())
.is_err()
);
}
#[test]
fn phase_from_fields() {
let fields = vec![
(
"m_name".into(),
InstanceField::String("New Construction".into()),
),
(
"m_sequence_number".into(),
InstanceField::Integer {
value: 1,
signed: false,
size: 4,
},
),
];
let decoded = DecodedElement {
id: None,
class: "Phase".into(),
fields,
byte_range: 0..0,
provenance: Default::default(),
};
let p = Phase::from_decoded(&decoded);
assert_eq!(p.name.as_deref(), Some("New Construction"));
assert_eq!(p.sequence_number, Some(1));
}
#[test]
fn design_option_primary_flag() {
let primary = DesignOption {
is_primary: Some(true),
..Default::default()
};
let alternative = DesignOption {
is_primary: Some(false),
..Default::default()
};
assert_eq!(primary.is_primary, Some(true));
assert_eq!(alternative.is_primary, Some(false));
}
#[test]
fn workset_modifiability() {
let modifiable = Workset {
is_open: Some(true),
is_editable: Some(true),
..Default::default()
};
let read_only = Workset {
is_open: Some(true),
is_editable: Some(false),
..Default::default()
};
let closed = Workset {
is_open: Some(false),
is_editable: Some(true),
..Default::default()
};
let unknown = Workset::default();
assert_eq!(modifiable.is_modifiable(), Some(true));
assert_eq!(read_only.is_modifiable(), Some(false));
assert_eq!(closed.is_modifiable(), Some(false));
assert_eq!(unknown.is_modifiable(), None);
}
#[test]
fn workset_guid_preserves() {
let fields = vec![
(
"m_unique_id".into(),
InstanceField::String("abc-123-def-456".into()),
),
("m_is_open".into(), InstanceField::Bool(true)),
];
let decoded = DecodedElement {
id: None,
class: "Workset".into(),
fields,
byte_range: 0..0,
provenance: Default::default(),
};
let w = Workset::from_decoded(&decoded);
assert_eq!(w.unique_id.as_deref(), Some("abc-123-def-456"));
assert_eq!(w.is_open, Some(true));
}
#[test]
fn empty_tolerance() {
let empty = DecodedElement {
id: None,
class: "Phase".into(),
fields: vec![],
byte_range: 0..0,
provenance: Default::default(),
};
assert!(Phase::from_decoded(&empty).name.is_none());
assert!(DesignOption::from_decoded(&empty).name.is_none());
assert!(Workset::from_decoded(&empty).name.is_none());
}
#[test]
fn class_names() {
assert_eq!(PhaseDecoder.class_name(), "Phase");
assert_eq!(DesignOptionDecoder.class_name(), "DesignOption");
assert_eq!(WorksetDecoder.class_name(), "Workset");
assert_eq!(RevisionDecoder.class_name(), "Revision");
}
#[test]
fn revision_rejects_wrong_schema() {
use crate::formats::ClassEntry;
let wrong = ClassEntry {
name: "Wall".into(),
offset: 0,
fields: vec![],
tag: None,
parent: None,
declared_field_count: None,
was_parent_only: false,
ancestor_tag: None,
};
assert!(
RevisionDecoder
.decode(&[], &wrong, &HandleIndex::new())
.is_err()
);
}
#[test]
fn revision_status_mapping() {
assert_eq!(RevisionStatus::from_code(0), RevisionStatus::Draft);
assert_eq!(RevisionStatus::from_code(1), RevisionStatus::Issued);
assert_eq!(RevisionStatus::from_code(42), RevisionStatus::Other);
}
#[test]
fn revision_from_decoded() {
let fields = vec![
(
"m_sequence_number".into(),
InstanceField::Integer {
value: 3,
signed: false,
size: 4,
},
),
(
"m_revision_date".into(),
InstanceField::String("2026-04-20".into()),
),
(
"m_description".into(),
InstanceField::String("Permit set".into()),
),
("m_issued_to".into(), InstanceField::String("Client".into())),
(
"m_issued_by".into(),
InstanceField::String("Architect of Record".into()),
),
(
"m_status".into(),
InstanceField::Integer {
value: 1,
signed: false,
size: 4,
},
),
];
let decoded = DecodedElement {
id: None,
class: "Revision".into(),
fields,
byte_range: 0..0,
provenance: Default::default(),
};
let r = Revision::from_decoded(&decoded);
assert_eq!(r.sequence_number, Some(3));
assert_eq!(r.revision_date.as_deref(), Some("2026-04-20"));
assert_eq!(r.description.as_deref(), Some("Permit set"));
assert_eq!(r.issued_to.as_deref(), Some("Client"));
assert_eq!(r.issued_by.as_deref(), Some("Architect of Record"));
assert_eq!(r.status, Some(RevisionStatus::Issued));
assert!(r.is_issued());
}
#[test]
fn revision_is_issued_when_unset_is_false() {
let r = Revision::default();
assert!(!r.is_issued());
}
}