use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DocumentIdentity {
pub document_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_guid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revit_year: Option<u32>,
}
impl DocumentIdentity {
pub fn from_key(document_key: impl Into<String>) -> Self {
Self {
document_key: document_key.into(),
file_guid: None,
revit_year: None,
}
}
pub fn with_file_meta(mut self, file_guid: Option<String>, revit_year: Option<u32>) -> Self {
self.file_guid = file_guid;
self.revit_year = revit_year;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UniqueId(pub String);
impl UniqueId {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for UniqueId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ScopedElementRef {
pub document: DocumentIdentity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub element_id: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unique_id: Option<UniqueId>,
}
impl ScopedElementRef {
pub fn from_element_id(document: DocumentIdentity, element_id: u32) -> Self {
Self {
document,
element_id: Some(element_id),
unique_id: None,
}
}
pub fn same_element_id(&self, other: &Self) -> bool {
self.document.document_key == other.document.document_key
&& self.element_id.is_some()
&& self.element_id == other.element_id
}
pub fn element_id_comparable_with(&self, other: &Self) -> bool {
self.document.document_key == other.document.document_key
&& self.element_id.is_some()
&& other.element_id.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SourceSpan {
pub stream: String,
pub start: u64,
pub end: u64,
}
impl SourceSpan {
pub fn new(stream: impl Into<String>, start: u64, end: u64) -> Option<Self> {
if end < start {
return None;
}
Some(Self {
stream: stream.into(),
start,
end,
})
}
pub fn len(&self) -> u64 {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RecordIdentity {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema_tag: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scoped_element_refs_require_same_document() {
let a = ScopedElementRef::from_element_id(DocumentIdentity::from_key("doc-a"), 42);
let b = ScopedElementRef::from_element_id(DocumentIdentity::from_key("doc-b"), 42);
let a2 = ScopedElementRef::from_element_id(DocumentIdentity::from_key("doc-a"), 42);
assert!(!a.element_id_comparable_with(&b));
assert!(!a.same_element_id(&b));
assert!(a.same_element_id(&a2));
}
#[test]
fn source_span_rejects_inverted_range() {
assert!(SourceSpan::new("Global/Latest", 10, 4).is_none());
let span = SourceSpan::new("Global/Latest", 4, 10).expect("valid");
assert_eq!(span.len(), 6);
}
#[test]
fn identity_round_trips_json() {
let doc = DocumentIdentity::from_key("fixture:es-remap-00").with_file_meta(
Some("d713e470-abcd-4321-9876-123456789012".into()),
Some(2024),
);
let refer = ScopedElementRef {
document: doc,
element_id: Some(7),
unique_id: Some(UniqueId("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into())),
};
let json = serde_json::to_value(&refer).expect("serialize");
let back: ScopedElementRef = serde_json::from_value(json).expect("deserialize");
assert_eq!(back, refer);
}
}