use crate::error::{PhysicsError, PhysicsResult};
use oxirs_core::model::{NamedNode, Term};
use oxirs_core::parser::{Parser, RdfFormat};
use oxirs_core::rdf_store::{QueryResults, RdfStore};
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
pub struct SammParser {
store: Arc<RdfStore>,
samm_prefix: String,
}
impl SammParser {
pub fn new() -> PhysicsResult<Self> {
let store = RdfStore::new()
.map_err(|e| PhysicsError::SammParsing(format!("Failed to create store: {}", e)))?;
Ok(Self {
store: Arc::new(store),
samm_prefix: "urn:samm:org.eclipse.esmf.samm:meta-model:2.0.0#".to_string(),
})
}
pub fn with_store(store: Arc<RdfStore>) -> Self {
Self {
store,
samm_prefix: "urn:samm:org.eclipse.esmf.samm:meta-model:2.0.0#".to_string(),
}
}
pub async fn parse_samm_file(&self, path: &Path) -> PhysicsResult<AspectModel> {
let content = std::fs::read_to_string(path)
.map_err(|e| PhysicsError::SammParsing(format!("Failed to read file: {}", e)))?;
self.parse_samm_string(&content).await
}
pub async fn parse_samm_string(&self, content: &str) -> PhysicsResult<AspectModel> {
let mut temp_store = RdfStore::new().map_err(|e| {
PhysicsError::SammParsing(format!("Failed to create temporary store: {}", e))
})?;
let parser = Parser::new(RdfFormat::Turtle);
let quads = parser
.parse_str_to_quads(content)
.map_err(|e| PhysicsError::SammParsing(format!("Failed to parse Turtle: {}", e)))?;
for quad in quads {
temp_store
.insert_quad(quad)
.map_err(|e| PhysicsError::SammParsing(format!("Failed to insert quad: {}", e)))?;
}
let temp_parser = SammParser::with_store(Arc::new(temp_store));
let entities = temp_parser.extract_entity_types().await?;
let properties = temp_parser.extract_properties().await?;
let relationships = temp_parser.extract_relationships().await?;
let constraints = temp_parser.extract_constraints().await?;
Ok(AspectModel {
entities,
properties,
relationships,
constraints,
})
}
pub async fn extract_entity_types(&self) -> PhysicsResult<Vec<EntityType>> {
let query = format!(
r#"
PREFIX samm: <{samm}>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?entity ?name ?description ?property WHERE {{
?entity a samm:Aspect .
OPTIONAL {{ ?entity rdfs:label ?name }}
OPTIONAL {{ ?entity samm:description ?description }}
OPTIONAL {{ ?entity samm:properties ?propList }}
OPTIONAL {{ ?propList rdf:rest*/rdf:first ?property }}
}}
"#,
samm = self.samm_prefix
);
let results = self
.store
.query(&query)
.map_err(|e| PhysicsError::SammParsing(format!("Entity query failed: {}", e)))?;
let mut entities_map: HashMap<String, EntityType> = HashMap::new();
if let QueryResults::Bindings(ref bindings) = results.results() {
for binding in bindings {
if let Some(Term::NamedNode(entity_node)) = binding.get("entity") {
let entity_uri = entity_node.as_str().to_string();
let name = binding
.get("name")
.and_then(|t| {
if let Term::Literal(lit) = t {
Some(lit.value().to_string())
} else {
None
}
})
.unwrap_or_else(|| {
entity_uri
.split('#')
.next_back()
.or_else(|| entity_uri.split('/').next_back())
.unwrap_or("unknown")
.to_string()
});
let description = binding.get("description").and_then(|t| {
if let Term::Literal(lit) = t {
Some(lit.value().to_string())
} else {
None
}
});
let entity =
entities_map
.entry(entity_uri.clone())
.or_insert_with(|| EntityType {
uri: entity_node.clone(),
name,
description,
properties: Vec::new(),
});
if let Some(Term::NamedNode(prop_node)) = binding.get("property") {
if !entity.properties.contains(prop_node) {
entity.properties.push(prop_node.clone());
}
}
}
}
}
Ok(entities_map.into_values().collect())
}
pub async fn extract_properties(&self) -> PhysicsResult<Vec<PropertyDefinition>> {
let query = format!(
r#"
PREFIX samm: <{samm}>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?property ?name ?datatype ?unit ?optional WHERE {{
?property a samm:Property .
OPTIONAL {{ ?property rdfs:label ?name }}
OPTIONAL {{ ?property samm:dataType ?datatype }}
OPTIONAL {{ ?property samm:characteristic ?char .
?char samm:unit ?unit }}
OPTIONAL {{ ?property samm:optional ?optional }}
}}
"#,
samm = self.samm_prefix
);
let results = self
.store
.query(&query)
.map_err(|e| PhysicsError::SammParsing(format!("Property query failed: {}", e)))?;
let mut properties = Vec::new();
if let QueryResults::Bindings(ref bindings) = results.results() {
for binding in bindings {
if let Some(Term::NamedNode(prop_node)) = binding.get("property") {
let name = binding
.get("name")
.and_then(|t| {
if let Term::Literal(lit) = t {
Some(lit.value().to_string())
} else {
None
}
})
.unwrap_or_else(|| {
prop_node
.as_str()
.split('#')
.next_back()
.or_else(|| prop_node.as_str().split('/').next_back())
.unwrap_or("unknown")
.to_string()
});
let datatype = binding
.get("datatype")
.and_then(|t| {
if let Term::NamedNode(dt) = t {
Some(dt.clone())
} else {
None
}
})
.unwrap_or_else(|| {
NamedNode::new("http://www.w3.org/2001/XMLSchema#string")
.expect("Invalid XSD string IRI")
});
let unit = binding.get("unit").and_then(|t| {
if let Term::Literal(lit) = t {
Some(lit.value().to_string())
} else if let Term::NamedNode(node) = t {
Some(
node.as_str()
.split('#')
.next_back()
.or_else(|| node.as_str().split('/').next_back())
.unwrap_or("dimensionless")
.to_string(),
)
} else {
None
}
});
let optional = binding
.get("optional")
.and_then(|t| {
if let Term::Literal(lit) = t {
lit.value().parse::<bool>().ok()
} else {
None
}
})
.unwrap_or(false);
properties.push(PropertyDefinition {
uri: prop_node.clone(),
name,
datatype,
unit,
optional,
});
}
}
}
Ok(properties)
}
pub async fn extract_relationships(&self) -> PhysicsResult<Vec<RelationshipDefinition>> {
let query = format!(
r#"
PREFIX samm: <{samm}>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?source ?predicate ?target ?name WHERE {{
?source ?predicate ?target .
FILTER(STRSTARTS(STR(?predicate), STR(samm:)))
FILTER(isIRI(?target))
OPTIONAL {{ ?predicate rdfs:label ?name }}
}}
"#,
samm = self.samm_prefix
);
let results = self
.store
.query(&query)
.map_err(|e| PhysicsError::SammParsing(format!("Relationship query failed: {}", e)))?;
let mut relationships = Vec::new();
if let QueryResults::Bindings(ref bindings) = results.results() {
for binding in bindings {
if let (
Some(Term::NamedNode(source)),
Some(Term::NamedNode(pred)),
Some(Term::NamedNode(target)),
) = (
binding.get("source"),
binding.get("predicate"),
binding.get("target"),
) {
let name = binding
.get("name")
.and_then(|t| {
if let Term::Literal(lit) = t {
Some(lit.value().to_string())
} else {
None
}
})
.unwrap_or_else(|| {
pred.as_str()
.split('#')
.next_back()
.or_else(|| pred.as_str().split('/').next_back())
.unwrap_or("unknown")
.to_string()
});
relationships.push(RelationshipDefinition {
source: source.clone(),
predicate: pred.clone(),
target: target.clone(),
name,
});
}
}
}
Ok(relationships)
}
pub async fn extract_constraints(&self) -> PhysicsResult<Vec<ConstraintDefinition>> {
let query = format!(
r#"
PREFIX samm: <{samm}>
PREFIX samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.0.0#>
SELECT ?property ?constraint ?value WHERE {{
?property samm:characteristic ?char .
?char a ?constraint .
OPTIONAL {{ ?char samm-c:minValue ?value }}
OPTIONAL {{ ?char samm-c:maxValue ?value }}
OPTIONAL {{ ?char samm-c:value ?value }}
FILTER(STRSTARTS(STR(?constraint), STR(samm-c:)))
}}
"#,
samm = self.samm_prefix
);
let results = self
.store
.query(&query)
.map_err(|e| PhysicsError::SammParsing(format!("Constraint query failed: {}", e)))?;
let mut constraints = Vec::new();
if let QueryResults::Bindings(ref bindings) = results.results() {
for binding in bindings {
if let (Some(Term::NamedNode(prop)), Some(Term::NamedNode(constraint_type))) =
(binding.get("property"), binding.get("constraint"))
{
if let Some(value_term) = binding.get("value") {
let constraint_type_str = constraint_type
.as_str()
.split('#')
.next_back()
.or_else(|| constraint_type.as_str().split('/').next_back())
.unwrap_or("unknown");
let constraint = match constraint_type_str {
"RangeConstraint" => {
if let Term::Literal(lit) = value_term {
if let Ok(val) = lit.value().parse::<f64>() {
ConstraintType::Range(val, val) } else {
continue;
}
} else {
continue;
}
}
"MinValueConstraint" => {
if let Term::Literal(lit) = value_term {
if let Ok(val) = lit.value().parse::<f64>() {
ConstraintType::MinValue(val)
} else {
continue;
}
} else {
continue;
}
}
"MaxValueConstraint" => {
if let Term::Literal(lit) = value_term {
if let Ok(val) = lit.value().parse::<f64>() {
ConstraintType::MaxValue(val)
} else {
continue;
}
} else {
continue;
}
}
_ => continue,
};
constraints.push(ConstraintDefinition {
property: prop.clone(),
constraint_type: constraint,
value: value_term.clone(),
});
}
}
}
}
Ok(constraints)
}
pub fn generate_sparql_query(&self, entity_type: &EntityType) -> String {
let mut select_vars = Vec::new();
let mut where_clauses = Vec::new();
for (idx, property) in entity_type.properties.iter().enumerate() {
let var_name = format!("prop{}", idx);
select_vars.push(format!("?{}", var_name));
let prop_local_name = property
.as_str()
.split('#')
.next_back()
.or_else(|| property.as_str().split('/').next_back())
.unwrap_or("unknown");
where_clauses.push(format!("?entity :{} ?{} .", prop_local_name, var_name));
}
format!(
r#"
SELECT ?entity {} WHERE {{
?entity a :{} .
{}
}}
"#,
select_vars.join(" "),
entity_type.name,
where_clauses.join("\n ")
)
}
pub fn validate_data(
&self,
property_values: &HashMap<String, f64>,
constraints: &[ConstraintDefinition],
) -> PhysicsResult<()> {
for constraint in constraints {
let prop_name = constraint
.property
.as_str()
.split('#')
.next_back()
.or_else(|| constraint.property.as_str().split('/').next_back())
.unwrap_or("unknown");
if let Some(&value) = property_values.get(prop_name) {
match &constraint.constraint_type {
ConstraintType::MinValue(min) if value < *min => {
return Err(PhysicsError::ConstraintViolation(format!(
"Property {} value {} is less than minimum {}",
prop_name, value, min
)));
}
ConstraintType::MaxValue(max) if value > *max => {
return Err(PhysicsError::ConstraintViolation(format!(
"Property {} value {} is greater than maximum {}",
prop_name, value, max
)));
}
ConstraintType::Range(min, max) if (value < *min || value > *max) => {
return Err(PhysicsError::ConstraintViolation(format!(
"Property {} value {} is outside range [{}, {}]",
prop_name, value, min, max
)));
}
_ => {}
}
}
}
Ok(())
}
}
impl Default for SammParser {
fn default() -> Self {
Self::new().expect("Failed to create default SAMM parser")
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AspectModel {
pub entities: Vec<EntityType>,
pub properties: Vec<PropertyDefinition>,
pub relationships: Vec<RelationshipDefinition>,
pub constraints: Vec<ConstraintDefinition>,
}
#[derive(Debug, Clone, Serialize)]
pub struct EntityType {
#[serde(skip)]
pub uri: NamedNode,
pub name: String,
pub description: Option<String>,
#[serde(skip)]
pub properties: Vec<NamedNode>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PropertyDefinition {
#[serde(skip)]
pub uri: NamedNode,
pub name: String,
#[serde(skip)]
pub datatype: NamedNode,
pub unit: Option<String>,
pub optional: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct RelationshipDefinition {
#[serde(skip)]
pub source: NamedNode,
#[serde(skip)]
pub predicate: NamedNode,
#[serde(skip)]
pub target: NamedNode,
pub name: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConstraintDefinition {
#[serde(skip)]
pub property: NamedNode,
pub constraint_type: ConstraintType,
#[serde(skip)]
pub value: Term,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum ConstraintType {
Range(f64, f64),
MinValue(f64),
MaxValue(f64),
Pattern(String),
EnumValues(Vec<String>),
}
#[cfg(test)]
mod tests {
use super::*;
use oxirs_core::model::Literal;
const SAMPLE_SAMM_TTL: &str = r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.0.0#> .
@prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.0.0#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix phys: <http://oxirs.org/physics#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
phys:RigidBody a samm:Aspect ;
rdfs:label "Rigid Body" ;
samm:description "A rigid body with physical properties" .
phys:mass a samm:Property ;
rdfs:label "mass" ;
samm:dataType xsd:double ;
samm:characteristic phys:MassCharacteristic .
phys:MassCharacteristic a samm-c:Measurement ;
samm:unit "kg" ;
samm-c:minValue "0.0"^^xsd:double .
phys:position a samm:Property ;
rdfs:label "position" ;
samm:dataType phys:Vector3D .
"#;
#[tokio::test]
async fn test_parse_samm_string() {
let parser = SammParser::new().expect("Failed to create parser");
let model = parser
.parse_samm_string(SAMPLE_SAMM_TTL)
.await
.expect("Failed to parse SAMM");
assert!(!model.entities.is_empty(), "Should have entities");
assert!(!model.properties.is_empty(), "Should have properties");
}
#[tokio::test]
async fn test_extract_entity_types() {
let parser = SammParser::new().expect("Failed to create parser");
let model = parser
.parse_samm_string(SAMPLE_SAMM_TTL)
.await
.expect("Failed to parse");
assert!(
!model.entities.is_empty(),
"Should have at least one entity"
);
let rigid_body = model.entities.iter().find(|e| e.name == "Rigid Body");
assert!(rigid_body.is_some(), "Should have RigidBody entity");
}
#[tokio::test]
async fn test_extract_properties() {
let parser = SammParser::new().expect("Failed to create parser");
let model = parser
.parse_samm_string(SAMPLE_SAMM_TTL)
.await
.expect("Failed to parse");
assert!(!model.properties.is_empty(), "Should have properties");
let mass_prop = model.properties.iter().find(|p| p.name == "mass");
assert!(mass_prop.is_some(), "Should have mass property");
}
#[tokio::test]
async fn test_extract_constraints() {
let parser = SammParser::new().expect("Failed to create parser");
let model = parser
.parse_samm_string(SAMPLE_SAMM_TTL)
.await
.expect("Failed to parse");
assert!(!model.properties.is_empty() || !model.entities.is_empty());
}
#[tokio::test]
async fn test_generate_sparql_query() {
let parser = SammParser::new().expect("Failed to create parser");
let mass_uri = NamedNode::new("http://oxirs.org/physics#mass").expect("Invalid URI");
let pos_uri = NamedNode::new("http://oxirs.org/physics#position").expect("Invalid URI");
let entity = EntityType {
uri: NamedNode::new("http://oxirs.org/physics#RigidBody").expect("Invalid URI"),
name: "RigidBody".to_string(),
description: Some("A rigid body".to_string()),
properties: vec![mass_uri, pos_uri],
};
let query = parser.generate_sparql_query(&entity);
assert!(query.contains("SELECT"), "Query should contain SELECT");
assert!(query.contains("?entity"), "Query should contain ?entity");
assert!(
query.contains("RigidBody"),
"Query should contain entity name"
);
}
#[test]
fn test_validate_data() {
let parser = SammParser::new().expect("Failed to create parser");
let mut values = HashMap::new();
values.insert("mass".to_string(), 10.0);
values.insert("temperature".to_string(), 300.0);
let mass_prop = NamedNode::new("http://oxirs.org/physics#mass").expect("Invalid URI");
let temp_prop =
NamedNode::new("http://oxirs.org/physics#temperature").expect("Invalid URI");
let constraints = vec![
ConstraintDefinition {
property: mass_prop.clone(),
constraint_type: ConstraintType::MinValue(0.0),
value: Term::Literal(Literal::new("0.0")),
},
ConstraintDefinition {
property: temp_prop.clone(),
constraint_type: ConstraintType::Range(0.0, 1000.0),
value: Term::Literal(Literal::new("0.0")),
},
];
assert!(parser.validate_data(&values, &constraints).is_ok());
let mut invalid_values = values.clone();
invalid_values.insert("mass".to_string(), -1.0);
assert!(parser.validate_data(&invalid_values, &constraints).is_err());
let mut invalid_values2 = values.clone();
invalid_values2.insert("temperature".to_string(), 1500.0);
assert!(parser
.validate_data(&invalid_values2, &constraints)
.is_err());
}
#[test]
fn test_constraint_types() {
let min_constraint = ConstraintType::MinValue(0.0);
let max_constraint = ConstraintType::MaxValue(100.0);
let range_constraint = ConstraintType::Range(0.0, 100.0);
assert_eq!(min_constraint, ConstraintType::MinValue(0.0));
assert_eq!(max_constraint, ConstraintType::MaxValue(100.0));
assert_eq!(range_constraint, ConstraintType::Range(0.0, 100.0));
}
#[test]
fn test_aspect_model_serialization() {
let model = AspectModel {
entities: Vec::new(),
properties: Vec::new(),
relationships: Vec::new(),
constraints: Vec::new(),
};
let json = serde_json::to_string(&model).expect("Failed to serialize");
assert!(json.contains("entities"));
assert!(json.contains("properties"));
assert!(json.contains("relationships"));
assert!(json.contains("constraints"));
}
}