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/XMI";
pub const XSI: &str = "http://www.w3.org/2001/XMLSchema-instance";
pub const KERML: &str = "https://www.omg.org/spec/KerML/20250201";
pub const SYSML: &str = "https://www.omg.org/spec/SysML/20250201";
}
#[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(())
}
}
impl Xmi {
#[cfg(feature = "interchange")]
pub fn read_from_path(
&self,
input: &[u8],
path: &std::path::Path,
) -> Result<Model, InterchangeError> {
XmiReader::new().read_with_path(input, Some(path))
}
}
#[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>>,
base_path: Option<std::path::PathBuf>,
href_name_cache: std::collections::HashMap<String, String>,
pending_rel_sources: std::collections::HashMap<String, (String, ElementKind)>,
declared_namespaces: std::collections::HashMap<String, String>,
}
#[derive(Debug)]
enum StackEntry {
Root,
Containment,
Element,
}
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(),
base_path: None,
href_name_cache: std::collections::HashMap::new(),
pending_rel_sources: std::collections::HashMap::new(),
declared_namespaces: std::collections::HashMap::new(),
}
}
pub fn read(&mut self, input: &[u8]) -> Result<Model, InterchangeError> {
self.read_with_path(input, None)
}
pub fn read_with_path(
&mut self,
input: &[u8],
path: Option<&std::path::Path>,
) -> Result<Model, InterchangeError> {
self.base_path = path.map(|p| p.parent().unwrap_or(p).to_path_buf());
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 self.depth_stack.is_empty() {
self.capture_namespace_declarations(e)?;
}
if tag_name == "xmi:XMI" || tag_name == "XMI" {
self.depth_stack.push(StackEntry::Root);
return Ok(());
}
let has_href = e
.attributes()
.any(|attr| attr.map(|a| a.key.as_ref() == b"href").unwrap_or(false));
if is_containment_tag(tag_name)
&& tag_name != "ownedRelationship"
&& tag_name != "ownedRelatedElement"
&& !has_href
{
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: Option<bool> = None;
let mut is_variation: Option<bool> = None;
let mut is_derived: Option<bool> = None;
let mut is_readonly: Option<bool> = None;
let mut is_parallel: Option<bool> = None;
let mut is_individual: Option<bool> = None;
let mut is_end: Option<bool> = None;
let mut is_default: Option<bool> = None;
let mut is_ordered: Option<bool> = None;
let mut is_nonunique: Option<bool> = None;
let mut is_portion: Option<bool> = None;
let mut is_standard: Option<bool> = None;
let mut is_composite: Option<bool> = None;
let mut is_unique: Option<bool> = None;
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" => 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 = Some(value == "true"),
"isVariation" => is_variation = Some(value == "true"),
"isDerived" => is_derived = Some(value == "true"),
"isReadOnly" => is_readonly = Some(value == "true"),
"isParallel" => is_parallel = Some(value == "true"),
"isIndividual" => is_individual = Some(value == "true"),
"isEnd" => is_end = Some(value == "true"),
"isDefault" => is_default = Some(value == "true"),
"isOrdered" => is_ordered = Some(value == "true"),
"isNonunique" => is_nonunique = Some(value == "true"),
"isPortion" => is_portion = Some(value == "true"),
"isStandard" => is_standard = Some(value == "true"),
"isComposite" => is_composite = Some(value == "true"),
"isUnique" => is_unique = Some(value == "true"),
"body" => body = Some(value),
"href" => href = Some(value),
"source" | "relatedElement" | "subclassifier" | "typedFeature"
| "redefiningFeature" | "subsettingFeature" | "typeDisjoined" => {
source_ref = Some(value.clone());
extra_attrs.push((key.to_string(), value));
}
"target" | "superclassifier" | "redefinedFeature" | "subsettedFeature"
| "general" | "specific" | "type" | "chainingFeature"
| "importedMembership" | "importedNamespace" | "disjoiningType"
| "originalType" | "memberElement" | "referencedFeature" => {
target_ref = Some(value.clone());
extra_attrs.push((key.to_string(), 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(ref t) = xmi_type {
element.properties.insert(
Arc::from("_xsi_type"),
PropertyValue::String(Arc::from(t.as_str())),
);
}
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()));
}
if let Some(val) = is_abstract {
element.set_abstract(val);
}
if let Some(val) = is_variation {
element.set_variation(val);
}
if let Some(val) = is_derived {
element.set_derived(val);
}
if let Some(val) = is_readonly {
element.set_readonly(val);
}
if let Some(val) = is_parallel {
element.set_parallel(val);
}
if let Some(val) = is_individual {
element.set_individual(val);
}
if let Some(val) = is_end {
element.set_end(val);
}
if let Some(val) = is_default {
element.set_default(val);
}
if let Some(val) = is_ordered {
element.set_ordered(val);
}
if let Some(val) = is_nonunique {
element.set_nonunique(val);
}
if let Some(val) = is_portion {
element.set_portion(val);
}
if let Some(val) = is_standard {
element
.properties
.insert(Arc::from("isStandard"), PropertyValue::Boolean(val));
}
if let Some(val) = is_composite {
element
.properties
.insert(Arc::from("isComposite"), PropertyValue::Boolean(val));
}
if let Some(val) = is_unique {
element
.properties
.insert(Arc::from("isUnique"), PropertyValue::Boolean(val));
}
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_default()
.push(id.clone());
}
if kind.is_relationship() {
if let (Some(src), Some(tgt)) = (
source_ref
.clone()
.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);
} else if let Some(src) = source_ref {
self.pending_rel_sources.insert(id.clone(), (src, kind));
}
}
self.elements_by_id.insert(id.clone(), element);
self.parent_stack.push(id);
self.depth_stack.push(StackEntry::Element);
} else if let Some(h) = href {
if let Some(parent_id) = self.parent_stack.last().cloned() {
let target_id = h.rsplit('#').next().map(|s| s.to_string());
let resolved_name = self.resolve_href_name(&h);
let fallback_name = if resolved_name.is_none() {
extract_name_from_href_path(&h)
} else {
None
};
if let Some(parent_elem) = self.elements_by_id.get_mut(&parent_id) {
if let Some(name) = resolved_name {
parent_elem.properties.insert(
Arc::from("href_target_name"),
PropertyValue::String(Arc::from(name.as_str())),
);
} else if let Some(name) = fallback_name {
parent_elem.properties.insert(
Arc::from("href_target_name"),
PropertyValue::String(Arc::from(name.as_str())),
);
}
parent_elem.properties.insert(
Arc::from("href"),
PropertyValue::String(Arc::from(h.as_str())),
);
parent_elem.properties.insert(
Arc::from("_href_tag"),
PropertyValue::String(Arc::from(tag_name)),
);
if let Some(ref t) = xmi_type {
parent_elem.properties.insert(
Arc::from("_href_xsi_type"),
PropertyValue::String(Arc::from(t.as_str())),
);
}
}
if let Some(target) = target_id {
if let Some((src, kind)) = self.pending_rel_sources.remove(&parent_id) {
let rel_kind = element_kind_to_relationship_kind(kind);
let relationship =
Relationship::new(parent_id.clone(), rel_kind, src, target);
self.relationships.push(relationship);
}
}
}
self.depth_stack.push(StackEntry::Containment);
} else {
self.depth_stack.push(StackEntry::Containment);
}
Ok(())
}
fn resolve_href_name(&mut self, href: &str) -> Option<String> {
if let Some(cached) = self.href_name_cache.get(href) {
return Some(cached.clone());
}
let hash_pos = href.rfind('#')?;
let path_part = &href[..hash_pos];
let element_id = &href[hash_pos + 1..];
let decoded_path = path_part.replace("%20", " ");
let base = self.base_path.as_ref()?;
let target_path = base.join(&decoded_path);
let file_content = std::fs::read(&target_path).ok()?;
let content_str = String::from_utf8_lossy(&file_content);
let id_pattern = format!(r#"xmi:id="{}""#, element_id);
if let Some(id_pos) = content_str.find(&id_pattern) {
let search_end = (id_pos + 500).min(content_str.len());
let search_slice = &content_str[id_pos..search_end];
let name = extract_attr_value(search_slice, "declaredName")
.or_else(|| extract_attr_value(search_slice, "name"));
if let Some(elem_name) = name {
let file_name = target_path.file_stem()?.to_str()?;
let qualified_name = format!("{}::{}", file_name, elem_name);
self.href_name_cache
.insert(href.to_string(), qualified_name.clone());
return Some(qualified_name);
}
}
None
}
fn handle_end_element(&mut self) {
if let Some(StackEntry::Element) = self.depth_stack.pop() {
self.parent_stack.pop();
}
}
fn capture_namespace_declarations(
&mut self,
e: &BytesStart<'_>,
) -> Result<(), InterchangeError> {
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}")))?;
if let Some(prefix) = key.strip_prefix("xmlns:") {
let value = attr
.unescape_value()
.map_err(|e| InterchangeError::xml(format!("Attribute value error: {e}")))?
.to_string();
self.declared_namespaces.insert(prefix.to_string(), value);
}
}
Ok(())
}
fn build_model(&mut self) -> Result<Model, InterchangeError> {
let mut model = Model::new();
model.metadata.declared_namespaces = std::mem::take(&mut self.declared_namespaces);
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 extract_attr_value(xml: &str, attr_name: &str) -> Option<String> {
let pattern = format!(r#"{}=""#, attr_name);
let start = xml.find(&pattern)? + pattern.len();
let remaining = &xml[start..];
let end = remaining.find('"')?;
Some(remaining[..end].to_string())
}
fn is_containment_tag(tag: &str) -> bool {
matches!(
tag,
"ownedMember"
| "ownedFeature"
| "ownedElement"
| "ownedImport"
| "member"
| "feature"
| "ownedSpecialization"
| "ownedSubsetting"
| "ownedRedefinition"
| "ownedTyping"
| "importedMembership"
| "superclassifier"
| "redefinedFeature"
| "subsettedFeature"
)
}
fn extract_name_from_href_path(href: &str) -> Option<String> {
if let Some(hash_pos) = href.rfind('#') {
let path = &href[..hash_pos];
let decoded_path = path.replace("%20", " ");
if let Some(file_start) = decoded_path.rfind('/') {
let file = &decoded_path[file_start + 1..];
if let Some(ext_pos) = file.rfind('.') {
return Some(file[..ext_pos].to_string());
}
} else if let Some(ext_pos) = decoded_path.rfind('.') {
return Some(decoded_path[..ext_pos].to_string());
}
}
None
}
fn element_kind_to_relationship_kind(kind: ElementKind) -> RelationshipKind {
match kind {
ElementKind::Specialization => RelationshipKind::Specialization,
ElementKind::FeatureTyping => RelationshipKind::FeatureTyping,
ElementKind::Subsetting
| ElementKind::ReferenceSubsetting
| ElementKind::CrossSubsetting => RelationshipKind::Subsetting,
ElementKind::Redefinition => RelationshipKind::Redefinition,
ElementKind::Import | ElementKind::NamespaceImport => RelationshipKind::NamespaceImport,
ElementKind::MembershipImport => RelationshipKind::MembershipImport,
ElementKind::Membership => RelationshipKind::Membership,
ElementKind::OwningMembership
| ElementKind::ReturnParameterMembership
| ElementKind::ParameterMembership
| ElementKind::EndFeatureMembership
| ElementKind::ResultExpressionMembership => RelationshipKind::OwningMembership,
ElementKind::FeatureMembership => RelationshipKind::FeatureMembership,
ElementKind::Conjugation => RelationshipKind::Conjugation,
ElementKind::FeatureChaining => RelationshipKind::FeatureChaining,
ElementKind::Disjoining => RelationshipKind::Disjoining,
_ => 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("ASCII"), None)))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
let roots: Vec<_> = model.iter_roots().collect();
if roots.len() == 1 {
let root = roots[0];
self.write_root_element(&mut writer, model, root)?;
} else if roots.is_empty() {
return Err(InterchangeError::xml("Model has no root elements"));
} else {
self.write_xmi_wrapper(&mut writer, model, &roots)?;
}
let mut output = buffer.into_inner();
output.push(b'\n');
Ok(output)
}
fn write_root_element<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
element: &Element,
) -> Result<(), InterchangeError> {
let type_name = Self::get_xmi_type(element);
let mut elem_start = BytesStart::new(&type_name);
elem_start.push_attribute(("xmi:version", "2.0"));
elem_start.push_attribute(("xmlns:xmi", namespace::XMI));
elem_start.push_attribute(("xmlns:xsi", namespace::XSI));
Self::write_namespace_attrs(&mut elem_start, model);
self.write_element_attrs(&mut elem_start, element, model);
let has_href = element.properties.get("href").is_some();
let has_children = !element.owned_elements.is_empty();
if has_href || has_children {
writer
.write_event(Event::Start(elem_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
Self::write_href_child(writer, element)?;
for child_id in &element.owned_elements {
if let Some(child) = model.get(child_id) {
self.write_owned_relationship(writer, model, child)?;
}
}
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(())
}
fn write_xmi_wrapper<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
roots: &[&Element],
) -> Result<(), InterchangeError> {
let mut xmi_start = BytesStart::new("xmi:XMI");
xmi_start.push_attribute(("xmi:version", "2.0"));
xmi_start.push_attribute(("xmlns:xmi", namespace::XMI));
xmi_start.push_attribute(("xmlns:xsi", namespace::XSI));
Self::write_namespace_attrs(&mut xmi_start, model);
writer
.write_event(Event::Start(xmi_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
for root in roots {
self.write_element_nested(writer, model, root)?;
}
writer
.write_event(Event::End(BytesEnd::new("xmi:XMI")))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
Ok(())
}
fn write_element_attrs(
&self,
elem_start: &mut BytesStart,
element: &Element,
_model: &Model,
) {
elem_start.push_attribute(("xmi:id", element.id.as_str()));
elem_start.push_attribute(("elementId", element.id.as_str()));
let uses_sysml_naming =
if let Some(super::super::model::PropertyValue::String(xsi_type)) =
element.properties.get("_xsi_type")
{
xsi_type.starts_with("sysml:")
} else {
element.kind.is_sysml()
};
if let Some(ref name) = element.name {
if uses_sysml_naming {
elem_start.push_attribute(("declaredName", name.as_ref()));
} else {
elem_start.push_attribute(("name", name.as_ref()));
}
}
if let Some(ref short_name) = element.short_name {
if uses_sysml_naming {
elem_start.push_attribute(("declaredShortName", short_name.as_ref()));
} else {
elem_start.push_attribute(("shortName", short_name.as_ref()));
}
}
if let Some(ref qn) = element.qualified_name {
elem_start.push_attribute(("qualifiedName", qn.as_ref()));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isAbstract")
{
elem_start.push_attribute(("isAbstract", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isVariation")
{
elem_start.push_attribute(("isVariation", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isDerived")
{
elem_start.push_attribute(("isDerived", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isReadOnly")
{
elem_start.push_attribute(("isReadOnly", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isParallel")
{
elem_start.push_attribute(("isParallel", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isIndividual")
{
elem_start.push_attribute(("isIndividual", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isEnd")
{
elem_start.push_attribute(("isEnd", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isDefault")
{
elem_start.push_attribute(("isDefault", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isNonunique")
{
elem_start.push_attribute(("isNonunique", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isPortion")
{
elem_start.push_attribute(("isPortion", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isUnique")
{
elem_start.push_attribute(("isUnique", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isOrdered")
{
elem_start.push_attribute(("isOrdered", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isComposite")
{
elem_start.push_attribute(("isComposite", if *v { "true" } else { "false" }));
}
if let Some(super::super::model::PropertyValue::Boolean(v)) =
element.properties.get("isStandard")
{
elem_start.push_attribute(("isStandard", if *v { "true" } else { "false" }));
}
if let Some(ref doc) = element.documentation {
let escaped = doc
.replace('&', "&") .replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\n', "
");
elem_start.push_attribute((b"body" as &[u8], escaped.as_bytes()));
}
for (key, value) in &element.properties {
let k = key.as_ref();
if k == "isAbstract"
|| k == "isVariation"
|| k == "isDerived"
|| k == "isReadOnly"
|| k == "isParallel"
|| k == "isIndividual"
|| k == "isEnd"
|| k == "isDefault"
|| k == "isOrdered"
|| k == "isNonunique"
|| k == "isPortion"
|| k == "isStandard"
|| k == "isComposite"
|| k == "isUnique"
|| k == "href"
|| k == "href_target_name"
|| k.starts_with("_")
{
continue;
}
if let super::super::model::PropertyValue::String(s) = value {
elem_start.push_attribute((k, s.as_ref()));
}
}
}
fn write_namespace_attrs(elem_start: &mut BytesStart, model: &Model) {
let ns = &model.metadata.declared_namespaces;
if ns.is_empty() {
elem_start.push_attribute(("xmlns:kerml", namespace::KERML));
elem_start.push_attribute(("xmlns:sysml", namespace::SYSML));
} else {
if let Some(uri) = ns.get("kerml") {
elem_start.push_attribute(("xmlns:kerml", uri.as_str()));
}
if let Some(uri) = ns.get("sysml") {
elem_start.push_attribute(("xmlns:sysml", uri.as_str()));
}
}
}
fn get_xmi_type(element: &Element) -> String {
if let Some(super::super::model::PropertyValue::String(orig)) =
element.properties.get("_xsi_type")
{
return orig.to_string();
}
element.kind.xmi_type().to_string()
}
fn href_element_name(kind: ElementKind) -> &'static str {
match kind {
ElementKind::NamespaceImport => "importedNamespace",
ElementKind::MembershipImport => "importedMembership",
ElementKind::Membership => "memberElement",
ElementKind::Specialization => "superclassifier",
ElementKind::FeatureTyping => "type",
ElementKind::Subsetting
| ElementKind::ReferenceSubsetting
| ElementKind::CrossSubsetting => "subsettedFeature",
ElementKind::Redefinition => "redefinedFeature",
ElementKind::Disjoining => "disjoiningType",
ElementKind::Conjugation => "originalType",
ElementKind::FeatureChaining => "chainingFeature",
_ => "target",
}
}
fn write_href_child<W: std::io::Write>(
writer: &mut Writer<W>,
element: &Element,
) -> Result<(), InterchangeError> {
if let Some(super::super::model::PropertyValue::String(href)) =
element.properties.get("href")
{
let href_elem_name: String =
if let Some(super::super::model::PropertyValue::String(tag)) =
element.properties.get("_href_tag")
{
tag.to_string()
} else {
Self::href_element_name(element.kind).to_string()
};
let mut href_elem = BytesStart::new(&href_elem_name);
if let Some(super::super::model::PropertyValue::String(xsi_type)) =
element.properties.get("_href_xsi_type")
{
href_elem.push_attribute(("xsi:type", xsi_type.as_ref()));
}
href_elem.push_attribute(("href", href.as_ref()));
writer
.write_event(Event::Empty(href_elem))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
}
Ok(())
}
fn write_owned_relationship<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
child: &Element,
) -> Result<(), InterchangeError> {
if child.kind.is_relationship() {
self.write_relationship_direct(writer, model, child)
} else {
self.write_non_relationship_wrapped(writer, model, child)
}
}
fn write_relationship_direct<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
child: &Element,
) -> Result<(), InterchangeError> {
let type_name = Self::get_xmi_type(child);
let mut rel_start = BytesStart::new("ownedRelationship");
rel_start.push_attribute(("xsi:type", type_name.as_str()));
self.write_element_attrs(&mut rel_start, child, model);
let has_href = child.properties.get("href").is_some();
let has_children = !child.owned_elements.is_empty();
if has_href || has_children {
writer
.write_event(Event::Start(rel_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
Self::write_href_child(writer, child)?;
for grandchild_id in &child.owned_elements {
if let Some(grandchild) = model.get(grandchild_id) {
self.write_owned_related_element(writer, model, grandchild)?;
}
}
writer
.write_event(Event::End(BytesEnd::new("ownedRelationship")))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
} else {
writer
.write_event(Event::Empty(rel_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
}
Ok(())
}
fn write_non_relationship_wrapped<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
child: &Element,
) -> Result<(), InterchangeError> {
self.write_owned_related_element(writer, model, child)
}
fn write_owned_related_element<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
element: &Element,
) -> Result<(), InterchangeError> {
let type_name = Self::get_xmi_type(element);
let mut elem_start = BytesStart::new("ownedRelatedElement");
elem_start.push_attribute(("xsi:type", type_name.as_str()));
self.write_element_attrs(&mut elem_start, element, model);
let has_href = element.properties.get("href").is_some();
let has_children = !element.owned_elements.is_empty();
if has_href || has_children {
writer
.write_event(Event::Start(elem_start))
.map_err(|e| InterchangeError::xml(format!("Write error: {e}")))?;
Self::write_href_child(writer, element)?;
for child_id in &element.owned_elements {
if let Some(child) = model.get(child_id) {
self.write_owned_relationship(writer, model, child)?;
}
}
writer
.write_event(Event::End(BytesEnd::new("ownedRelatedElement")))
.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(())
}
fn write_element_nested<W: std::io::Write>(
&self,
writer: &mut Writer<W>,
model: &Model,
element: &Element,
) -> Result<(), InterchangeError> {
let type_name = Self::get_xmi_type(element);
let mut elem_start = BytesStart::new(&type_name);
self.write_element_attrs(&mut elem_start, element, model);
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) {
self.write_owned_relationship(writer, model, child)?;
}
}
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(())
}
}
}
#[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(),
))
}
}
#[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!(
ElementKind::from_xmi_type("sysml:Package"),
ElementKind::Package
);
assert_eq!(
ElementKind::from_xmi_type("sysml:PartDefinition"),
ElementKind::PartDefinition
);
assert_eq!(
ElementKind::from_xmi_type("kerml:Feature"),
ElementKind::Feature
);
}
#[test]
fn test_relationship_kind_from_xmi() {
assert_eq!(
RelationshipKind::from_xmi_type("kerml:Specialization"),
Some(RelationshipKind::Specialization)
);
assert_eq!(
RelationshipKind::from_xmi_type("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("sysml:Package"),
"Missing sysml:Package. Got:\n{}",
output_str
);
assert!(
output_str.contains(r#"xmi:id="pkg1""#),
"Missing xmi:id. Got:\n{}",
output_str
);
assert!(
output_str.contains(r#"declaredName="TestPackage""#),
"Missing declaredName. Got:\n{}",
output_str
);
}
#[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);
}
#[test]
fn test_xmi_read_is_abstract() {
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:PartDefinition xmi:id="pd1" name="AbstractPart" isAbstract="true"/>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
let elem = model
.get(&ElementId::new("pd1"))
.expect("Element not found");
assert!(elem.is_abstract, "isAbstract should be true");
}
#[test]
fn test_xmi_read_is_variation() {
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:PartDefinition xmi:id="pd1" name="VariantPart" isVariation="true"/>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
let elem = model
.get(&ElementId::new("pd1"))
.expect("Element not found");
assert!(elem.is_variation, "isVariation should be true");
}
#[test]
fn test_xmi_read_is_derived() {
let xmi_content = br#"<?xml version="1.0" encoding="UTF-8"?>
<xmi:XMI xmlns:xmi="http://www.omg.org/spec/XMI/20131001"
xmlns:kerml="http://www.omg.org/spec/KerML/20230201">
<kerml:Feature xmi:id="f1" name="derivedFeature" isDerived="true"/>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
let elem = model.get(&ElementId::new("f1")).expect("Element not found");
assert!(elem.is_derived, "isDerived should be true");
}
#[test]
fn test_xmi_read_is_readonly() {
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:AttributeUsage xmi:id="a1" name="constantValue" isReadOnly="true"/>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
let elem = model.get(&ElementId::new("a1")).expect("Element not found");
assert!(elem.is_readonly, "isReadOnly should be true");
}
#[test]
fn test_xmi_read_is_parallel() {
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:StateUsage xmi:id="s1" name="parallelState" isParallel="true"/>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
let elem = model.get(&ElementId::new("s1")).expect("Element not found");
assert!(elem.is_parallel, "isParallel should be true");
}
#[test]
fn test_xmi_write_modifiers() {
let mut model = Model::new();
let mut elem = Element::new("pd1", ElementKind::PartDefinition);
elem.name = Some("TestPart".into());
elem.set_abstract(true);
elem.set_variation(true);
model.add_element(elem);
let mut feat = Element::new("f1", ElementKind::Feature);
feat.name = Some("TestFeature".into());
feat.set_derived(true);
feat.set_readonly(true);
model.add_element(feat);
let mut state = Element::new("s1", ElementKind::StateUsage);
state.name = Some("TestState".into());
state.set_parallel(true);
model.add_element(state);
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(r#"isAbstract="true""#),
"Should contain isAbstract"
);
assert!(
output_str.contains(r#"isVariation="true""#),
"Should contain isVariation"
);
assert!(
output_str.contains(r#"isDerived="true""#),
"Should contain isDerived"
);
assert!(
output_str.contains(r#"isReadOnly="true""#),
"Should contain isReadOnly"
);
assert!(
output_str.contains(r#"isParallel="true""#),
"Should contain isParallel"
);
}
#[test]
fn test_xmi_roundtrip_modifiers() {
let mut model = Model::new();
let mut elem = Element::new("pd1", ElementKind::PartDefinition);
elem.name = Some("AbstractVariation".into());
elem.set_abstract(true);
elem.set_variation(true);
model.add_element(elem);
let mut feat = Element::new("f1", ElementKind::AttributeUsage);
feat.name = Some("DerivedReadonly".into());
feat.set_derived(true);
feat.set_readonly(true);
model.add_element(feat);
let mut state = Element::new("s1", ElementKind::StateUsage);
state.name = Some("ParallelState".into());
state.set_parallel(true);
model.add_element(state);
let xmi_bytes = Xmi.write(&model).expect("Write failed");
let model2 = Xmi.read(&xmi_bytes).expect("Read failed");
let elem2 = model2.get(&ElementId::new("pd1")).unwrap();
assert!(elem2.is_abstract, "isAbstract not preserved");
assert!(elem2.is_variation, "isVariation not preserved");
let feat2 = model2.get(&ElementId::new("f1")).unwrap();
assert!(feat2.is_derived, "isDerived not preserved");
assert!(feat2.is_readonly, "isReadOnly not preserved");
let state2 = model2.get(&ElementId::new("s1")).unwrap();
assert!(state2.is_parallel, "isParallel not preserved");
}
#[test]
fn test_xmi_modifiers_default_false() {
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:PartDefinition xmi:id="pd1" name="NormalPart"/>
</xmi:XMI>"#;
let model = Xmi.read(xmi_content).expect("Failed to read XMI");
let elem = model
.get(&ElementId::new("pd1"))
.expect("Element not found");
assert!(!elem.is_abstract, "isAbstract should default to false");
assert!(!elem.is_variation, "isVariation should default to false");
assert!(!elem.is_derived, "isDerived should default to false");
assert!(!elem.is_readonly, "isReadOnly should default to false");
assert!(!elem.is_parallel, "isParallel should default to false");
}
#[test]
fn test_membership_import_href_child_roundtrip() {
let input = r#"<?xml version="1.0" encoding="ASCII"?>
<sysml:Namespace xmi:version="2.0" xmlns:xmi="http://www.omg.org/spec/XMI/20131001" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sysml="https://www.omg.org/spec/SysML/20250201" xmi:id="ns1" elementId="ns1">
<ownedRelationship xsi:type="sysml:MembershipImport" xmi:id="aed4e039-574f-5c98-83de-3aca582b628a" elementId="aed4e039-574f-5c98-83de-3aca582b628a">
<importedMembership href="../../Kernel%20Libraries/Kernel%20Data%20Type%20Library/ScalarValues.kermlx#a9e3be1d-4057-5cda-bdc0-eff9df4b33ea"/>
</ownedRelationship>
</sysml:Namespace>"#;
let model = Xmi.read(input.as_bytes()).expect("Failed to read XMI");
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("<importedMembership href="),
"Output must contain <importedMembership href=...> child element.\nGot:\n{}",
output_str
);
}
#[test]
fn test_namespace_import_href_child_roundtrip() {
let input = r#"<?xml version="1.0" encoding="ASCII"?>
<sysml:Namespace xmi:version="2.0" xmlns:xmi="http://www.omg.org/spec/XMI/20131001" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sysml="https://www.omg.org/spec/SysML/20250201" xmi:id="ns1" elementId="ns1">
<ownedRelationship xsi:type="sysml:NamespaceImport" xmi:id="4c829288-5c6b-5120-967f-9415c466b325" elementId="4c829288-5c6b-5120-967f-9415c466b325">
<importedNamespace href="../../Kernel%20Libraries/Kernel%20Data%20Type%20Library/Collections.kermlx#9837d4a5-c753-58a8-b614-16d4cb5fac19"/>
</ownedRelationship>
</sysml:Namespace>"#;
let model = Xmi.read(input.as_bytes()).expect("Failed to read XMI");
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("<importedNamespace href="),
"Output must contain <importedNamespace href=...> child element.\nGot:\n{}",
output_str
);
}
#[test]
fn test_documentation_body_newline_escaping() {
let input = r#"<?xml version="1.0" encoding="ASCII"?>
<sysml:Documentation xmi:version="2.0" xmlns:xmi="http://www.omg.org/spec/XMI/20131001" xmlns:sysml="https://www.omg.org/spec/SysML/20250201" xmi:id="doc1" elementId="doc1" body="Line one.
Line two.
"/>"#;
let model = Xmi.read(input.as_bytes()).expect("Failed to read XMI");
let doc = model.get(&ElementId::new("doc1")).expect("doc not found");
assert_eq!(
doc.documentation.as_deref(),
Some("Line one.\nLine two.\n"),
"Newlines should be parsed from 
"
);
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("
"),
"Output must contain 
 entity for newlines.\nGot:\n{}",
output_str
);
assert!(
!output_str.contains("&#xA;"),
"Output must NOT double-escape to &#xA;.\nGot:\n{}",
output_str
);
}
}
}