use std::sync::Arc;
use super::model::{Element, ElementId, ElementKind, Model, Relationship, RelationshipKind};
use super::{FormatCapability, InterchangeError, ModelFormat};
pub mod namespace {
pub const XMI: &str = "http://www.omg.org/spec/XMI/20131001";
pub const KERML: &str = "http://www.omg.org/spec/KerML/20230201";
pub const SYSML: &str = "http://www.omg.org/spec/SysML/20230201";
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Xmi;
impl ModelFormat for Xmi {
fn name(&self) -> &'static str {
"XMI"
}
fn extensions(&self) -> &'static [&'static str] {
&["xmi"]
}
fn mime_type(&self) -> &'static str {
"application/xmi+xml"
}
fn capabilities(&self) -> FormatCapability {
FormatCapability::FULL
}
fn read(&self, input: &[u8]) -> Result<Model, InterchangeError> {
#[cfg(feature = "interchange")]
{
XmiReader::new().read(input)
}
#[cfg(not(feature = "interchange"))]
{
let _ = input;
Err(InterchangeError::Unsupported(
"XMI reading requires the 'interchange' feature".to_string(),
))
}
}
fn write(&self, model: &Model) -> Result<Vec<u8>, InterchangeError> {
#[cfg(feature = "interchange")]
{
XmiWriter::new().write(model)
}
#[cfg(not(feature = "interchange"))]
{
let _ = model;
Err(InterchangeError::Unsupported(
"XMI writing requires the 'interchange' feature".to_string(),
))
}
}
fn validate(&self, input: &[u8]) -> Result<(), InterchangeError> {
let content = std::str::from_utf8(input)
.map_err(|e| InterchangeError::xml(format!("Invalid UTF-8: {e}")))?;
if !content.contains("xmi:XMI")
&& !content.contains("XMI")
&& !content.contains("sysml:Namespace")
&& !content.contains("kerml:Namespace")
{
return Err(InterchangeError::xml("Missing XMI/SysML root element"));
}
Ok(())
}
}
#[cfg(feature = "interchange")]
mod reader {
use super::super::model::PropertyValue;
use super::*;
use indexmap::IndexMap;
use quick_xml::Reader;
use quick_xml::events::{BytesStart, Event};
pub struct XmiReader {
elements_by_id: IndexMap<String, Element>,
parent_stack: Vec<String>,
depth_stack: Vec<StackEntry>,
relationships: Vec<Relationship>,
rel_counter: u32,
children_in_order: IndexMap<String, Vec<String>>,
}
#[derive(Debug)]
enum StackEntry {
Root,
Containment,
Element(String),
}
impl XmiReader {
pub fn new() -> Self {
Self {
elements_by_id: IndexMap::new(),
parent_stack: Vec::new(),
depth_stack: Vec::new(),
relationships: Vec::new(),
rel_counter: 0,
children_in_order: IndexMap::new(),
}
}
pub fn read(&mut self, input: &[u8]) -> Result<Model, InterchangeError> {
let mut reader = Reader::from_reader(input);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
self.handle_start_element(e)?;
}
Ok(Event::Empty(ref e)) => {
self.handle_start_element(e)?;
self.handle_end_element();
}
Ok(Event::End(_)) => {
self.handle_end_element();
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(InterchangeError::xml(format!(
"XML parse error at position {}: {e}",
reader.error_position()
)));
}
_ => {}
}
buf.clear();
}
self.build_model()
}
fn handle_start_element(&mut self, e: &BytesStart<'_>) -> Result<(), InterchangeError> {
let name_bytes = e.name();
let tag_name = std::str::from_utf8(name_bytes.as_ref())
.map_err(|e| InterchangeError::xml(format!("Invalid tag name: {e}")))?;
if tag_name == "xmi:XMI"
|| tag_name == "XMI"
|| tag_name == "sysml:Namespace"
|| tag_name == "kerml:Namespace"
{
self.depth_stack.push(StackEntry::Root);
return Ok(());
}
if is_containment_tag(tag_name)
&& tag_name != "ownedRelationship"
&& tag_name != "ownedRelatedElement"
{
self.depth_stack.push(StackEntry::Containment);
return Ok(());
}
let mut xmi_id: Option<String> = None;
let mut xmi_type: Option<String> = None;
let mut name: Option<String> = None;
let mut qualified_name: Option<String> = None;
let mut short_name: Option<String> = None;
let mut element_id: Option<String> = None;
let mut is_abstract = false;
let mut is_standard = false;
let mut is_composite = false;
let mut body: Option<String> = None;
let mut href: Option<String> = None;
let mut extra_attrs: Vec<(String, String)> = Vec::new();
let mut source_ref: Option<String> = None;
let mut target_ref: Option<String> = None;
for attr_result in e.attributes() {
let attr = attr_result
.map_err(|e| InterchangeError::xml(format!("Attribute error: {e}")))?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| InterchangeError::xml(format!("Attribute key error: {e}")))?;
let value = attr
.unescape_value()
.map_err(|e| InterchangeError::xml(format!("Attribute value error: {e}")))?
.to_string();
match key {
"xmi:id" | "id" => xmi_id = Some(value),
"xmi:type" | "xsi:type" | "type" => xmi_type = Some(value),
"name" | "declaredName" => name = Some(value),
"qualifiedName" => qualified_name = Some(value),
"shortName" | "declaredShortName" => short_name = Some(value),
"elementId" => element_id = Some(value),
"isAbstract" => is_abstract = value == "true",
"isStandard" => is_standard = value == "true",
"isComposite" => is_composite = value == "true",
"body" => body = Some(value),
"href" => href = Some(value),
"source" | "relatedElement" | "subclassifier" | "typedFeature"
| "redefiningFeature" | "subsettingFeature" => source_ref = Some(value),
"target" | "superclassifier" | "redefinedFeature" | "subsettedFeature"
| "general" | "specific" => target_ref = Some(value),
_ => {
if !key.starts_with("xmlns") && !key.starts_with("xmi:version") {
extra_attrs.push((key.to_string(), value));
}
}
}
}
if xmi_id.is_none() {
xmi_id = element_id.clone();
}
let type_str = xmi_type.as_deref().unwrap_or(tag_name);
let kind = ElementKind::from_xmi_type(type_str);
if let Some(id) = xmi_id {
let mut element = Element::new(id.clone(), kind);
if let Some(n) = name {
element.name = Some(Arc::from(n.as_str()));
}
if let Some(qn) = qualified_name {
element.qualified_name = Some(Arc::from(qn.as_str()));
}
if let Some(sn) = short_name {
element.short_name = Some(Arc::from(sn.as_str()));
}
element.is_abstract = is_abstract;
if is_standard {
element
.properties
.insert(Arc::from("isStandard"), PropertyValue::Boolean(true));
}
if is_composite {
element
.properties
.insert(Arc::from("isComposite"), PropertyValue::Boolean(true));
}
if let Some(b) = body {
element.documentation = Some(Arc::from(b.as_str()));
}
if let Some(h) = href {
element.properties.insert(
Arc::from("href"),
PropertyValue::String(Arc::from(h.as_str())),
);
}
for (key, value) in extra_attrs {
element.properties.insert(
Arc::from(key.as_str()),
PropertyValue::String(Arc::from(value.as_str())),
);
}
if let Some(parent_id) = self.parent_stack.last() {
element.owner = Some(ElementId::new(parent_id.clone()));
self.children_in_order
.entry(parent_id.clone())
.or_insert_with(Vec::new)
.push(id.clone());
}
if kind.is_relationship() {
if let (Some(src), Some(tgt)) = (
source_ref.or_else(|| self.parent_stack.last().cloned()),
target_ref,
) {
let rel_kind = element_kind_to_relationship_kind(kind);
let relationship = Relationship::new(id.clone(), rel_kind, src, tgt);
self.relationships.push(relationship);
}
}
self.elements_by_id.insert(id.clone(), element);
self.parent_stack.push(id.clone());
self.depth_stack.push(StackEntry::Element(id));
} else {
self.depth_stack.push(StackEntry::Containment);
}
Ok(())
}
fn handle_end_element(&mut self) {
if let Some(entry) = self.depth_stack.pop() {
if let StackEntry::Element(_) = entry {
self.parent_stack.pop();
}
}
}
fn build_model(&mut self) -> Result<Model, InterchangeError> {
let mut model = Model::new();
for (_, element) in self.elements_by_id.drain(..) {
model.add_element(element);
}
for rel in self.relationships.drain(..) {
model.add_relationship(rel);
}
for (parent_id, child_ids) in self.children_in_order.drain(..) {
if let Some(owner) = model.elements.get_mut(&ElementId::new(parent_id)) {
for child_id in child_ids {
owner.owned_elements.push(ElementId::new(child_id));
}
}
}
Ok(model)
}
#[allow(dead_code)]
fn next_rel_id(&mut self) -> ElementId {
self.rel_counter += 1;
ElementId::new(format!("_rel_{}", self.rel_counter))
}
}
fn is_containment_tag(tag: &str) -> bool {
matches!(
tag,
"ownedMember"
| "ownedFeature"
| "ownedElement"
| "ownedImport"
| "member"
| "feature"
| "ownedSpecialization"
| "ownedSubsetting"
| "ownedRedefinition"
| "ownedTyping"
| "importedMembership"
| "superclassifier"
| "redefinedFeature"
| "subsettedFeature"
)
}
fn element_kind_to_relationship_kind(kind: ElementKind) -> RelationshipKind {
match kind {
ElementKind::Specialization => RelationshipKind::Specialization,
ElementKind::FeatureTyping => RelationshipKind::FeatureTyping,
ElementKind::Subsetting => RelationshipKind::Subsetting,
ElementKind::Redefinition => RelationshipKind::Redefinition,
ElementKind::Import | ElementKind::NamespaceImport => RelationshipKind::NamespaceImport,
ElementKind::MembershipImport => RelationshipKind::MembershipImport,
ElementKind::Membership => RelationshipKind::Membership,
ElementKind::OwningMembership => RelationshipKind::OwningMembership,
ElementKind::FeatureMembership => RelationshipKind::FeatureMembership,
ElementKind::Conjugation => RelationshipKind::Conjugation,
_ => RelationshipKind::Dependency, }
}
}
#[cfg(feature = "interchange")]
use reader::XmiReader;
#[cfg(feature = "interchange")]
mod writer {
use super::*;
use quick_xml::Writer;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use std::io::Cursor;
pub struct XmiWriter;
impl XmiWriter {
pub fn new() -> Self {
Self
}
pub fn write(&self, model: &Model) -> Result<Vec<u8>, InterchangeError> {
let mut buffer = Cursor::new(Vec::new());
let mut writer = Writer::new_with_indent(&mut buffer, b' ', 2);
writer
.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
let mut xmi_start = BytesStart::new("xmi:XMI");
xmi_start.push_attribute(("xmlns:xmi", namespace::XMI));
xmi_start.push_attribute(("xmlns:kerml", namespace::KERML));
xmi_start.push_attribute(("xmlns:sysml", namespace::SYSML));
writer
.write_event(Event::Start(xmi_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
for root in model.iter_roots() {
self.write_element(&mut writer, model, root)?;
}
writer
.write_event(Event::End(BytesEnd::new("xmi:XMI")))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
Ok(buffer.into_inner())
}
fn write_element<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
element: &Element,
) -> Result<(), InterchangeError> {
let type_name = element.kind.xmi_type();
let mut elem_start = BytesStart::new(type_name);
elem_start.push_attribute(("xmi:id", element.id.as_str()));
if let Some(ref name) = element.name {
elem_start.push_attribute(("name", name.as_ref()));
}
if let Some(ref qualified_name) = element.qualified_name {
elem_start.push_attribute(("qualifiedName", qualified_name.as_ref()));
}
if let Some(ref short_name) = element.short_name {
elem_start.push_attribute(("shortName", short_name.as_ref()));
}
if element.is_abstract {
elem_start.push_attribute(("isAbstract", "true"));
}
if let Some(super::super::model::PropertyValue::Boolean(true)) =
element.properties.get("isStandard")
{
elem_start.push_attribute(("isStandard", "true"));
}
if let Some(super::super::model::PropertyValue::Boolean(true)) =
element.properties.get("isComposite")
{
elem_start.push_attribute(("isComposite", "true"));
}
if let Some(ref doc) = element.documentation {
elem_start.push_attribute(("body", doc.as_ref()));
}
if let Some(super::super::model::PropertyValue::String(href)) =
element.properties.get("href")
{
elem_start.push_attribute(("href", href.as_ref()));
}
for (key, value) in &element.properties {
if key.as_ref() == "isStandard"
|| key.as_ref() == "isComposite"
|| key.as_ref() == "href"
{
continue;
}
if let super::super::model::PropertyValue::String(s) = value {
elem_start.push_attribute((key.as_ref(), s.as_ref()));
}
}
let has_children = !element.owned_elements.is_empty();
if has_children {
writer
.write_event(Event::Start(elem_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
for child_id in &element.owned_elements {
if let Some(child) = model.get(child_id) {
let wrapper = if child.kind.is_relationship() {
"ownedRelationship"
} else {
"ownedMember"
};
writer
.write_event(Event::Start(BytesStart::new(wrapper)))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
self.write_element(writer, model, child)?;
writer
.write_event(Event::End(BytesEnd::new(wrapper)))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
}
}
writer
.write_event(Event::End(BytesEnd::new(type_name)))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
} else {
writer
.write_event(Event::Empty(elem_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
}
Ok(())
}
#[allow(dead_code)]
fn write_relationship<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
_model: &Model,
rel: &Relationship,
) -> Result<(), InterchangeError> {
writer
.write_event(Event::Start(BytesStart::new("ownedRelationship")))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
let rel_type = relationship_kind_to_xmi_type(rel.kind);
let mut rel_start = BytesStart::new(rel_type);
rel_start.push_attribute(("xmi:id", rel.id.as_str()));
match rel.kind {
RelationshipKind::Specialization => {
rel_start.push_attribute(("subclassifier", rel.source.as_str()));
rel_start.push_attribute(("superclassifier", rel.target.as_str()));
}
RelationshipKind::FeatureTyping => {
rel_start.push_attribute(("typedFeature", rel.source.as_str()));
rel_start.push_attribute(("type", rel.target.as_str()));
}
RelationshipKind::Redefinition => {
rel_start.push_attribute(("redefiningFeature", rel.source.as_str()));
rel_start.push_attribute(("redefinedFeature", rel.target.as_str()));
}
RelationshipKind::Subsetting => {
rel_start.push_attribute(("subsettingFeature", rel.source.as_str()));
rel_start.push_attribute(("subsettedFeature", rel.target.as_str()));
}
_ => {
rel_start.push_attribute(("source", rel.source.as_str()));
rel_start.push_attribute(("target", rel.target.as_str()));
}
}
writer
.write_event(Event::Empty(rel_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
writer
.write_event(Event::End(BytesEnd::new("ownedRelationship")))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
Ok(())
}
}
fn relationship_kind_to_xmi_type(kind: RelationshipKind) -> &'static str {
match kind {
RelationshipKind::Specialization => "kerml:Specialization",
RelationshipKind::FeatureTyping => "kerml:FeatureTyping",
RelationshipKind::Subsetting => "kerml:Subsetting",
RelationshipKind::Redefinition => "kerml:Redefinition",
RelationshipKind::NamespaceImport => "kerml:NamespaceImport",
RelationshipKind::MembershipImport => "kerml:MembershipImport",
RelationshipKind::Membership => "kerml:Membership",
RelationshipKind::OwningMembership => "kerml:OwningMembership",
RelationshipKind::FeatureMembership => "kerml:FeatureMembership",
RelationshipKind::Conjugation => "kerml:Conjugation",
RelationshipKind::Dependency => "kerml:Dependency",
_ => "kerml:Relationship", }
}
}
#[cfg(feature = "interchange")]
use writer::XmiWriter;
#[cfg(not(feature = "interchange"))]
struct XmiReader;
#[cfg(not(feature = "interchange"))]
impl XmiReader {
fn new() -> Self {
Self
}
fn read(&mut self, _input: &[u8]) -> Result<Model, InterchangeError> {
Err(InterchangeError::Unsupported(
"XMI reading requires the 'interchange' feature".to_string(),
))
}
}
#[cfg(not(feature = "interchange"))]
struct XmiWriter;
#[cfg(not(feature = "interchange"))]
impl XmiWriter {
fn new() -> Self {
Self
}
fn write(&self, _model: &Model) -> Result<Vec<u8>, InterchangeError> {
Err(InterchangeError::Unsupported(
"XMI writing requires the 'interchange' feature".to_string(),
))
}
}
#[allow(dead_code)]
pub fn element_kind_from_xmi(xmi_type: &str) -> ElementKind {
ElementKind::from_xmi_type(xmi_type)
}
#[allow(dead_code)]
pub fn element_kind_to_xmi(kind: ElementKind) -> &'static str {
kind.xmi_type()
}
#[allow(dead_code)]
pub fn relationship_kind_from_xmi(xmi_type: &str) -> Option<RelationshipKind> {
let type_name = xmi_type.split(':').last().unwrap_or(xmi_type);
match type_name {
"Specialization" => Some(RelationshipKind::Specialization),
"FeatureTyping" => Some(RelationshipKind::FeatureTyping),
"Subsetting" => Some(RelationshipKind::Subsetting),
"Redefinition" => Some(RelationshipKind::Redefinition),
"Conjugation" => Some(RelationshipKind::Conjugation),
"Membership" => Some(RelationshipKind::Membership),
"OwningMembership" => Some(RelationshipKind::OwningMembership),
"FeatureMembership" => Some(RelationshipKind::FeatureMembership),
"NamespaceImport" => Some(RelationshipKind::NamespaceImport),
"MembershipImport" => Some(RelationshipKind::MembershipImport),
"Dependency" => Some(RelationshipKind::Dependency),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xmi_format_metadata() {
let xmi = Xmi;
assert_eq!(xmi.name(), "XMI");
assert_eq!(xmi.extensions(), &["xmi"]);
assert_eq!(xmi.mime_type(), "application/xmi+xml");
assert!(xmi.capabilities().read);
assert!(xmi.capabilities().write);
}
#[test]
fn test_xmi_validate_valid() {
let xmi = Xmi;
let input =
br#"<?xml version="1.0"?><xmi:XMI xmlns:xmi="http://www.omg.org/spec/XMI/20131001"/>"#;
assert!(xmi.validate(input).is_ok());
}
#[test]
fn test_xmi_validate_invalid() {
let xmi = Xmi;
let input = b"<root>not xmi</root>";
assert!(xmi.validate(input).is_err());
}
#[test]
fn test_element_kind_from_xmi() {
assert_eq!(element_kind_from_xmi("sysml:Package"), ElementKind::Package);
assert_eq!(
element_kind_from_xmi("sysml:PartDefinition"),
ElementKind::PartDefinition
);
assert_eq!(element_kind_from_xmi("kerml:Feature"), ElementKind::Feature);
}
#[test]
fn test_relationship_kind_from_xmi() {
assert_eq!(
relationship_kind_from_xmi("kerml:Specialization"),
Some(RelationshipKind::Specialization)
);
assert_eq!(
relationship_kind_from_xmi("kerml:FeatureTyping"),
Some(RelationshipKind::FeatureTyping)
);
}
#[cfg(feature = "interchange")]
mod interchange_tests {
use super::*;
#[test]
fn test_xmi_read_simple_package() {
let xmi_content = br#"<?xml version="1.0" encoding="UTF-8"?>
<xmi:XMI xmlns:xmi="http://www.omg.org/spec/XMI/20131001"
xmlns:sysml="http://www.omg.org/spec/SysML/20230201">
<sysml:Package xmi:id="pkg1" name="MyPackage"/>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
assert_eq!(model.element_count(), 1);
let pkg = model
.get(&ElementId::new("pkg1"))
.expect("Package not found");
assert_eq!(pkg.name.as_deref(), Some("MyPackage"));
assert_eq!(pkg.kind, ElementKind::Package);
}
#[test]
fn test_xmi_read_nested_elements() {
let xmi_content = br#"<?xml version="1.0" encoding="UTF-8"?>
<xmi:XMI xmlns:xmi="http://www.omg.org/spec/XMI/20131001"
xmlns:sysml="http://www.omg.org/spec/SysML/20230201">
<sysml:Package xmi:id="pkg1" name="Vehicles">
<ownedMember>
<sysml:PartDefinition xmi:id="pd1" name="Car"/>
</ownedMember>
<ownedMember>
<sysml:PartDefinition xmi:id="pd2" name="Truck"/>
</ownedMember>
</sysml:Package>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
assert_eq!(model.element_count(), 3);
let pkg = model
.get(&ElementId::new("pkg1"))
.expect("Package not found");
assert_eq!(pkg.owned_elements.len(), 2);
let car = model.get(&ElementId::new("pd1")).expect("Car not found");
assert_eq!(car.name.as_deref(), Some("Car"));
assert_eq!(car.kind, ElementKind::PartDefinition);
assert_eq!(car.owner.as_ref().map(|id| id.as_str()), Some("pkg1"));
}
#[test]
fn test_xmi_write_simple_model() {
let mut model = Model::new();
model.add_element(Element::new("pkg1", ElementKind::Package).with_name("TestPackage"));
let output = Xmi.write(&model).expect("Failed to write XMI");
let output_str = String::from_utf8(output).expect("Invalid UTF-8");
assert!(output_str.contains("xmi:XMI"));
assert!(output_str.contains("sysml:Package"));
assert!(output_str.contains(r#"xmi:id="pkg1""#));
assert!(output_str.contains(r#"name="TestPackage""#));
}
#[test]
fn test_xmi_roundtrip() {
let mut model = Model::new();
let pkg = Element::new("pkg1", ElementKind::Package).with_name("RoundtripTest");
model.add_element(pkg);
let part = Element::new("part1", ElementKind::PartDefinition)
.with_name("Vehicle")
.with_owner("pkg1");
model.add_element(part);
if let Some(pkg) = model.elements.get_mut(&ElementId::new("pkg1")) {
pkg.owned_elements.push(ElementId::new("part1"));
}
let xmi_bytes = Xmi.write(&model).expect("Write failed");
let model2 = Xmi.read(&xmi_bytes).expect("Read failed");
assert_eq!(model2.element_count(), 2);
let pkg2 = model2.get(&ElementId::new("pkg1")).unwrap();
assert_eq!(pkg2.name.as_deref(), Some("RoundtripTest"));
assert_eq!(pkg2.owned_elements.len(), 1);
}
}
}