use crate::error::{Result, SammError};
use crate::metamodel::{
Aspect, Characteristic, CharacteristicKind, ModelElement, Operation, Property,
};
use std::path::Path;
use tokio::fs;
pub struct TurtleSerializer {
indent_size: usize,
}
impl TurtleSerializer {
pub fn new() -> Self {
Self { indent_size: 2 }
}
pub async fn serialize_to_file(&self, aspect: &Aspect, path: &Path) -> Result<()> {
let content = self.serialize_to_string(aspect)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(path, content).await?;
Ok(())
}
pub fn serialize_to_string(&self, aspect: &Aspect) -> Result<String> {
let mut output = String::new();
output.push_str(&self.generate_prefixes());
output.push('\n');
output.push_str(&self.serialize_aspect(aspect)?);
for property in aspect.properties() {
output.push('\n');
output.push_str(&self.serialize_property(property)?);
}
for operation in aspect.operations() {
output.push('\n');
output.push_str(&self.serialize_operation(operation)?);
}
Ok(output)
}
fn generate_prefixes(&self) -> String {
let mut prefixes = String::new();
prefixes.push_str("@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.1.0#> .\n");
prefixes
.push_str("@prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.1.0#> .\n");
prefixes.push_str("@prefix samm-e: <urn:samm:org.eclipse.esmf.samm:entity:2.1.0#> .\n");
prefixes.push_str("@prefix unit: <urn:samm:org.eclipse.esmf.samm:unit:2.1.0#> .\n");
prefixes.push_str("@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n");
prefixes.push_str("@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n");
prefixes.push_str("@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n");
prefixes
}
fn serialize_aspect(&self, aspect: &Aspect) -> Result<String> {
let mut output = String::new();
let metadata = aspect.metadata();
output.push_str(&format!("<{}> a samm:Aspect", metadata.urn));
if !metadata.preferred_names.is_empty() {
for (lang, name) in &metadata.preferred_names {
output.push_str(&format!(
" ;\n samm:preferredName \"{}\"@{}",
self.escape_string(name),
lang
));
}
}
if !metadata.descriptions.is_empty() {
for (lang, desc) in &metadata.descriptions {
output.push_str(&format!(
" ;\n samm:description \"{}\"@{}",
self.escape_string(desc),
lang
));
}
}
if !aspect.properties().is_empty() {
output.push_str(" ;\n samm:properties (");
for (i, prop) in aspect.properties().iter().enumerate() {
if i > 0 {
output.push(' ');
}
output.push_str(&format!("<{}>", prop.metadata().urn));
}
output.push(')');
}
if !aspect.operations().is_empty() {
output.push_str(" ;\n samm:operations (");
for (i, op) in aspect.operations().iter().enumerate() {
if i > 0 {
output.push(' ');
}
output.push_str(&format!("<{}>", op.metadata().urn));
}
output.push(')');
}
output.push_str(" .\n");
Ok(output)
}
fn serialize_property(&self, property: &Property) -> Result<String> {
let mut output = String::new();
let metadata = property.metadata();
output.push_str(&format!("<{}> a samm:Property", metadata.urn));
if !metadata.preferred_names.is_empty() {
for (lang, name) in &metadata.preferred_names {
output.push_str(&format!(
" ;\n samm:preferredName \"{}\"@{}",
self.escape_string(name),
lang
));
}
}
if !metadata.descriptions.is_empty() {
for (lang, desc) in &metadata.descriptions {
output.push_str(&format!(
" ;\n samm:description \"{}\"@{}",
self.escape_string(desc),
lang
));
}
}
if let Some(characteristic) = &property.characteristic {
output.push_str(&format!(
" ;\n samm:characteristic <{}>",
characteristic.urn()
));
}
if property.optional {
output.push_str(" ;\n samm:optional \"true\"^^xsd:boolean");
}
output.push_str(" .\n");
if let Some(characteristic) = &property.characteristic {
output.push('\n');
output.push_str(&self.serialize_characteristic(characteristic)?);
}
Ok(output)
}
fn serialize_operation(&self, operation: &Operation) -> Result<String> {
let mut output = String::new();
let metadata = operation.metadata();
output.push_str(&format!("<{}> a samm:Operation", metadata.urn));
if !metadata.preferred_names.is_empty() {
for (lang, name) in &metadata.preferred_names {
output.push_str(&format!(
" ;\n samm:preferredName \"{}\"@{}",
self.escape_string(name),
lang
));
}
}
if !metadata.descriptions.is_empty() {
for (lang, desc) in &metadata.descriptions {
output.push_str(&format!(
" ;\n samm:description \"{}\"@{}",
self.escape_string(desc),
lang
));
}
}
if !operation.input.is_empty() {
output.push_str(" ;\n samm:input (");
for (i, prop) in operation.input.iter().enumerate() {
if i > 0 {
output.push(' ');
}
output.push_str(&format!("<{}>", prop.metadata().urn));
}
output.push(')');
}
if let Some(output_prop) = &operation.output {
output.push_str(&format!(
" ;\n samm:output <{}>",
output_prop.metadata().urn
));
}
output.push_str(" .\n");
Ok(output)
}
fn serialize_characteristic(&self, characteristic: &Characteristic) -> Result<String> {
let mut output = String::new();
let char_type = match &characteristic.kind {
CharacteristicKind::Trait => "samm-c:Trait",
CharacteristicKind::Quantifiable { .. } => "samm-c:Quantifiable",
CharacteristicKind::Measurement { .. } => "samm-c:Measurement",
CharacteristicKind::Enumeration { .. } => "samm-c:Enumeration",
CharacteristicKind::State { .. } => "samm-c:State",
CharacteristicKind::Duration { .. } => "samm-c:Duration",
CharacteristicKind::Collection { .. } => "samm-c:Collection",
CharacteristicKind::List { .. } => "samm-c:List",
CharacteristicKind::Set { .. } => "samm-c:Set",
CharacteristicKind::SortedSet { .. } => "samm-c:SortedSet",
CharacteristicKind::TimeSeries { .. } => "samm-c:TimeSeries",
CharacteristicKind::Either { .. } => "samm-c:Either",
CharacteristicKind::SingleEntity { .. } => "samm-c:SingleEntity",
CharacteristicKind::StructuredValue { .. } => "samm-c:StructuredValue",
CharacteristicKind::Code => "samm-c:Code",
};
output.push_str(&format!("<{}> a {}", characteristic.urn(), char_type));
if let Some(data_type) = &characteristic.data_type {
output.push_str(&format!(" ;\n samm:dataType <{}>", data_type));
}
output.push_str(" .\n");
Ok(output)
}
fn escape_string(&self, s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
}
impl Default for TurtleSerializer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metamodel::{CharacteristicKind, ElementMetadata};
#[test]
fn test_serialize_minimal_aspect() {
let mut metadata = ElementMetadata::new("urn:test:aspect#MinimalAspect".to_string());
metadata.add_preferred_name("en".to_string(), "Minimal Aspect".to_string());
let aspect = Aspect {
metadata,
properties: vec![],
operations: vec![],
events: vec![],
};
let serializer = TurtleSerializer::new();
let result = serializer.serialize_to_string(&aspect);
assert!(result.is_ok());
let ttl = result.expect("result should be Ok");
assert!(ttl.contains("@prefix samm:"));
assert!(ttl.contains("<urn:test:aspect#MinimalAspect> a samm:Aspect"));
assert!(ttl.contains("samm:preferredName \"Minimal Aspect\"@en"));
}
#[test]
fn test_serialize_aspect_with_property() {
let mut aspect_meta = ElementMetadata::new("urn:test:aspect#TestAspect".to_string());
aspect_meta.add_preferred_name("en".to_string(), "Test Aspect".to_string());
let mut prop_meta = ElementMetadata::new("urn:test:property#temperature".to_string());
prop_meta.add_preferred_name("en".to_string(), "temperature".to_string());
let characteristic = Characteristic::new(
"urn:test:characteristic#TemperatureCharacteristic".to_string(),
CharacteristicKind::Trait,
)
.with_data_type("xsd:float".to_string());
let property = Property {
metadata: prop_meta,
characteristic: Some(characteristic),
example_values: vec![],
optional: false,
is_collection: false,
payload_name: None,
is_abstract: false,
extends: None,
};
let mut aspect = Aspect {
metadata: aspect_meta,
properties: vec![],
operations: vec![],
events: vec![],
};
aspect.add_property(property);
let serializer = TurtleSerializer::new();
let result = serializer.serialize_to_string(&aspect);
assert!(result.is_ok());
let ttl = result.expect("result should be Ok");
assert!(ttl.contains("<urn:test:aspect#TestAspect> a samm:Aspect"));
assert!(ttl.contains("samm:properties"));
assert!(ttl.contains("<urn:test:property#temperature>"));
assert!(ttl.contains("a samm:Property"));
assert!(ttl.contains("samm:characteristic"));
}
#[test]
fn test_escape_string() {
let serializer = TurtleSerializer::new();
assert_eq!(serializer.escape_string("test\"quote"), "test\\\"quote");
assert_eq!(serializer.escape_string("test\\slash"), "test\\\\slash");
assert_eq!(serializer.escape_string("test\nline"), "test\\nline");
}
}