oxirs_physics/simulation/
samm_parser.rs1use crate::error::{PhysicsError, PhysicsResult};
7use oxirs_core::model::{NamedNode, Term};
8use oxirs_core::parser::{Parser, RdfFormat};
9use oxirs_core::rdf_store::{QueryResults, RdfStore};
10use serde::Serialize;
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::Arc;
14
15pub struct SammParser {
17 store: Arc<RdfStore>,
19 samm_prefix: String,
21}
22
23impl SammParser {
24 pub fn new() -> PhysicsResult<Self> {
26 let store = RdfStore::new()
27 .map_err(|e| PhysicsError::SammParsing(format!("Failed to create store: {}", e)))?;
28
29 Ok(Self {
30 store: Arc::new(store),
31 samm_prefix: "urn:samm:org.eclipse.esmf.samm:meta-model:2.0.0#".to_string(),
32 })
33 }
34
35 pub fn with_store(store: Arc<RdfStore>) -> Self {
37 Self {
38 store,
39 samm_prefix: "urn:samm:org.eclipse.esmf.samm:meta-model:2.0.0#".to_string(),
40 }
41 }
42
43 pub async fn parse_samm_file(&self, path: &Path) -> PhysicsResult<AspectModel> {
45 let content = std::fs::read_to_string(path)
47 .map_err(|e| PhysicsError::SammParsing(format!("Failed to read file: {}", e)))?;
48
49 self.parse_samm_string(&content).await
51 }
52
53 pub async fn parse_samm_string(&self, content: &str) -> PhysicsResult<AspectModel> {
55 let mut temp_store = RdfStore::new().map_err(|e| {
57 PhysicsError::SammParsing(format!("Failed to create temporary store: {}", e))
58 })?;
59
60 let parser = Parser::new(RdfFormat::Turtle);
62 let quads = parser
63 .parse_str_to_quads(content)
64 .map_err(|e| PhysicsError::SammParsing(format!("Failed to parse Turtle: {}", e)))?;
65
66 for quad in quads {
68 temp_store
69 .insert_quad(quad)
70 .map_err(|e| PhysicsError::SammParsing(format!("Failed to insert quad: {}", e)))?;
71 }
72
73 let temp_parser = SammParser::with_store(Arc::new(temp_store));
75
76 let entities = temp_parser.extract_entity_types().await?;
78 let properties = temp_parser.extract_properties().await?;
79 let relationships = temp_parser.extract_relationships().await?;
80 let constraints = temp_parser.extract_constraints().await?;
81
82 Ok(AspectModel {
83 entities,
84 properties,
85 relationships,
86 constraints,
87 })
88 }
89
90 pub async fn extract_entity_types(&self) -> PhysicsResult<Vec<EntityType>> {
92 let query = format!(
93 r#"
94 PREFIX samm: <{samm}>
95 PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
96 PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
97
98 SELECT ?entity ?name ?description ?property WHERE {{
99 ?entity a samm:Aspect .
100 OPTIONAL {{ ?entity rdfs:label ?name }}
101 OPTIONAL {{ ?entity samm:description ?description }}
102 OPTIONAL {{ ?entity samm:properties ?propList }}
103 OPTIONAL {{ ?propList rdf:rest*/rdf:first ?property }}
104 }}
105 "#,
106 samm = self.samm_prefix
107 );
108
109 let results = self
110 .store
111 .query(&query)
112 .map_err(|e| PhysicsError::SammParsing(format!("Entity query failed: {}", e)))?;
113
114 let mut entities_map: HashMap<String, EntityType> = HashMap::new();
115
116 if let QueryResults::Bindings(ref bindings) = results.results() {
117 for binding in bindings {
118 if let Some(Term::NamedNode(entity_node)) = binding.get("entity") {
119 let entity_uri = entity_node.as_str().to_string();
120
121 let name = binding
122 .get("name")
123 .and_then(|t| {
124 if let Term::Literal(lit) = t {
125 Some(lit.value().to_string())
126 } else {
127 None
128 }
129 })
130 .unwrap_or_else(|| {
131 entity_uri
132 .split('#')
133 .next_back()
134 .or_else(|| entity_uri.split('/').next_back())
135 .unwrap_or("unknown")
136 .to_string()
137 });
138
139 let description = binding.get("description").and_then(|t| {
140 if let Term::Literal(lit) = t {
141 Some(lit.value().to_string())
142 } else {
143 None
144 }
145 });
146
147 let entity =
148 entities_map
149 .entry(entity_uri.clone())
150 .or_insert_with(|| EntityType {
151 uri: entity_node.clone(),
152 name,
153 description,
154 properties: Vec::new(),
155 });
156
157 if let Some(Term::NamedNode(prop_node)) = binding.get("property") {
159 if !entity.properties.contains(prop_node) {
160 entity.properties.push(prop_node.clone());
161 }
162 }
163 }
164 }
165 }
166
167 Ok(entities_map.into_values().collect())
168 }
169
170 pub async fn extract_properties(&self) -> PhysicsResult<Vec<PropertyDefinition>> {
172 let query = format!(
173 r#"
174 PREFIX samm: <{samm}>
175 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
176 PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
177
178 SELECT ?property ?name ?datatype ?unit ?optional WHERE {{
179 ?property a samm:Property .
180 OPTIONAL {{ ?property rdfs:label ?name }}
181 OPTIONAL {{ ?property samm:dataType ?datatype }}
182 OPTIONAL {{ ?property samm:characteristic ?char .
183 ?char samm:unit ?unit }}
184 OPTIONAL {{ ?property samm:optional ?optional }}
185 }}
186 "#,
187 samm = self.samm_prefix
188 );
189
190 let results = self
191 .store
192 .query(&query)
193 .map_err(|e| PhysicsError::SammParsing(format!("Property query failed: {}", e)))?;
194
195 let mut properties = Vec::new();
196
197 if let QueryResults::Bindings(ref bindings) = results.results() {
198 for binding in bindings {
199 if let Some(Term::NamedNode(prop_node)) = binding.get("property") {
200 let name = binding
201 .get("name")
202 .and_then(|t| {
203 if let Term::Literal(lit) = t {
204 Some(lit.value().to_string())
205 } else {
206 None
207 }
208 })
209 .unwrap_or_else(|| {
210 prop_node
211 .as_str()
212 .split('#')
213 .next_back()
214 .or_else(|| prop_node.as_str().split('/').next_back())
215 .unwrap_or("unknown")
216 .to_string()
217 });
218
219 let datatype = binding
220 .get("datatype")
221 .and_then(|t| {
222 if let Term::NamedNode(dt) = t {
223 Some(dt.clone())
224 } else {
225 None
226 }
227 })
228 .unwrap_or_else(|| {
229 NamedNode::new("http://www.w3.org/2001/XMLSchema#string")
230 .expect("Invalid XSD string IRI")
231 });
232
233 let unit = binding.get("unit").and_then(|t| {
234 if let Term::Literal(lit) = t {
235 Some(lit.value().to_string())
236 } else if let Term::NamedNode(node) = t {
237 Some(
238 node.as_str()
239 .split('#')
240 .next_back()
241 .or_else(|| node.as_str().split('/').next_back())
242 .unwrap_or("dimensionless")
243 .to_string(),
244 )
245 } else {
246 None
247 }
248 });
249
250 let optional = binding
251 .get("optional")
252 .and_then(|t| {
253 if let Term::Literal(lit) = t {
254 lit.value().parse::<bool>().ok()
255 } else {
256 None
257 }
258 })
259 .unwrap_or(false);
260
261 properties.push(PropertyDefinition {
262 uri: prop_node.clone(),
263 name,
264 datatype,
265 unit,
266 optional,
267 });
268 }
269 }
270 }
271
272 Ok(properties)
273 }
274
275 pub async fn extract_relationships(&self) -> PhysicsResult<Vec<RelationshipDefinition>> {
277 let query = format!(
278 r#"
279 PREFIX samm: <{samm}>
280 PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
281
282 SELECT ?source ?predicate ?target ?name WHERE {{
283 ?source ?predicate ?target .
284 FILTER(STRSTARTS(STR(?predicate), STR(samm:)))
285 FILTER(isIRI(?target))
286 OPTIONAL {{ ?predicate rdfs:label ?name }}
287 }}
288 "#,
289 samm = self.samm_prefix
290 );
291
292 let results = self
293 .store
294 .query(&query)
295 .map_err(|e| PhysicsError::SammParsing(format!("Relationship query failed: {}", e)))?;
296
297 let mut relationships = Vec::new();
298
299 if let QueryResults::Bindings(ref bindings) = results.results() {
300 for binding in bindings {
301 if let (
302 Some(Term::NamedNode(source)),
303 Some(Term::NamedNode(pred)),
304 Some(Term::NamedNode(target)),
305 ) = (
306 binding.get("source"),
307 binding.get("predicate"),
308 binding.get("target"),
309 ) {
310 let name = binding
311 .get("name")
312 .and_then(|t| {
313 if let Term::Literal(lit) = t {
314 Some(lit.value().to_string())
315 } else {
316 None
317 }
318 })
319 .unwrap_or_else(|| {
320 pred.as_str()
321 .split('#')
322 .next_back()
323 .or_else(|| pred.as_str().split('/').next_back())
324 .unwrap_or("unknown")
325 .to_string()
326 });
327
328 relationships.push(RelationshipDefinition {
329 source: source.clone(),
330 predicate: pred.clone(),
331 target: target.clone(),
332 name,
333 });
334 }
335 }
336 }
337
338 Ok(relationships)
339 }
340
341 pub async fn extract_constraints(&self) -> PhysicsResult<Vec<ConstraintDefinition>> {
343 let query = format!(
344 r#"
345 PREFIX samm: <{samm}>
346 PREFIX samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.0.0#>
347
348 SELECT ?property ?constraint ?value WHERE {{
349 ?property samm:characteristic ?char .
350 ?char a ?constraint .
351 OPTIONAL {{ ?char samm-c:minValue ?value }}
352 OPTIONAL {{ ?char samm-c:maxValue ?value }}
353 OPTIONAL {{ ?char samm-c:value ?value }}
354 FILTER(STRSTARTS(STR(?constraint), STR(samm-c:)))
355 }}
356 "#,
357 samm = self.samm_prefix
358 );
359
360 let results = self
361 .store
362 .query(&query)
363 .map_err(|e| PhysicsError::SammParsing(format!("Constraint query failed: {}", e)))?;
364
365 let mut constraints = Vec::new();
366
367 if let QueryResults::Bindings(ref bindings) = results.results() {
368 for binding in bindings {
369 if let (Some(Term::NamedNode(prop)), Some(Term::NamedNode(constraint_type))) =
370 (binding.get("property"), binding.get("constraint"))
371 {
372 if let Some(value_term) = binding.get("value") {
373 let constraint_type_str = constraint_type
374 .as_str()
375 .split('#')
376 .next_back()
377 .or_else(|| constraint_type.as_str().split('/').next_back())
378 .unwrap_or("unknown");
379
380 let constraint = match constraint_type_str {
381 "RangeConstraint" => {
382 if let Term::Literal(lit) = value_term {
383 if let Ok(val) = lit.value().parse::<f64>() {
384 ConstraintType::Range(val, val) } else {
386 continue;
387 }
388 } else {
389 continue;
390 }
391 }
392 "MinValueConstraint" => {
393 if let Term::Literal(lit) = value_term {
394 if let Ok(val) = lit.value().parse::<f64>() {
395 ConstraintType::MinValue(val)
396 } else {
397 continue;
398 }
399 } else {
400 continue;
401 }
402 }
403 "MaxValueConstraint" => {
404 if let Term::Literal(lit) = value_term {
405 if let Ok(val) = lit.value().parse::<f64>() {
406 ConstraintType::MaxValue(val)
407 } else {
408 continue;
409 }
410 } else {
411 continue;
412 }
413 }
414 _ => continue,
415 };
416
417 constraints.push(ConstraintDefinition {
418 property: prop.clone(),
419 constraint_type: constraint,
420 value: value_term.clone(),
421 });
422 }
423 }
424 }
425 }
426
427 Ok(constraints)
428 }
429
430 pub fn generate_sparql_query(&self, entity_type: &EntityType) -> String {
432 let mut select_vars = Vec::new();
433 let mut where_clauses = Vec::new();
434
435 for (idx, property) in entity_type.properties.iter().enumerate() {
436 let var_name = format!("prop{}", idx);
437 select_vars.push(format!("?{}", var_name));
438
439 let prop_local_name = property
440 .as_str()
441 .split('#')
442 .next_back()
443 .or_else(|| property.as_str().split('/').next_back())
444 .unwrap_or("unknown");
445
446 where_clauses.push(format!("?entity :{} ?{} .", prop_local_name, var_name));
447 }
448
449 format!(
450 r#"
451 SELECT ?entity {} WHERE {{
452 ?entity a :{} .
453 {}
454 }}
455 "#,
456 select_vars.join(" "),
457 entity_type.name,
458 where_clauses.join("\n ")
459 )
460 }
461
462 pub fn validate_data(
464 &self,
465 property_values: &HashMap<String, f64>,
466 constraints: &[ConstraintDefinition],
467 ) -> PhysicsResult<()> {
468 for constraint in constraints {
469 let prop_name = constraint
470 .property
471 .as_str()
472 .split('#')
473 .next_back()
474 .or_else(|| constraint.property.as_str().split('/').next_back())
475 .unwrap_or("unknown");
476
477 if let Some(&value) = property_values.get(prop_name) {
478 match &constraint.constraint_type {
479 ConstraintType::MinValue(min) if value < *min => {
480 return Err(PhysicsError::ConstraintViolation(format!(
481 "Property {} value {} is less than minimum {}",
482 prop_name, value, min
483 )));
484 }
485 ConstraintType::MaxValue(max) if value > *max => {
486 return Err(PhysicsError::ConstraintViolation(format!(
487 "Property {} value {} is greater than maximum {}",
488 prop_name, value, max
489 )));
490 }
491 ConstraintType::Range(min, max) if (value < *min || value > *max) => {
492 return Err(PhysicsError::ConstraintViolation(format!(
493 "Property {} value {} is outside range [{}, {}]",
494 prop_name, value, min, max
495 )));
496 }
497 _ => {}
498 }
499 }
500 }
501
502 Ok(())
503 }
504}
505
506impl Default for SammParser {
507 fn default() -> Self {
508 Self::new().expect("Failed to create default SAMM parser")
509 }
510}
511
512#[derive(Debug, Clone, Serialize)]
514pub struct AspectModel {
515 pub entities: Vec<EntityType>,
516 pub properties: Vec<PropertyDefinition>,
517 pub relationships: Vec<RelationshipDefinition>,
518 pub constraints: Vec<ConstraintDefinition>,
519}
520
521#[derive(Debug, Clone, Serialize)]
523pub struct EntityType {
524 #[serde(skip)]
525 pub uri: NamedNode,
526 pub name: String,
527 pub description: Option<String>,
528 #[serde(skip)]
529 pub properties: Vec<NamedNode>,
530}
531
532#[derive(Debug, Clone, Serialize)]
534pub struct PropertyDefinition {
535 #[serde(skip)]
536 pub uri: NamedNode,
537 pub name: String,
538 #[serde(skip)]
539 pub datatype: NamedNode,
540 pub unit: Option<String>,
541 pub optional: bool,
542}
543
544#[derive(Debug, Clone, Serialize)]
546pub struct RelationshipDefinition {
547 #[serde(skip)]
548 pub source: NamedNode,
549 #[serde(skip)]
550 pub predicate: NamedNode,
551 #[serde(skip)]
552 pub target: NamedNode,
553 pub name: String,
554}
555
556#[derive(Debug, Clone, Serialize)]
558pub struct ConstraintDefinition {
559 #[serde(skip)]
560 pub property: NamedNode,
561 pub constraint_type: ConstraintType,
562 #[serde(skip)]
563 pub value: Term,
564}
565
566#[derive(Debug, Clone, Serialize, PartialEq)]
568pub enum ConstraintType {
569 Range(f64, f64),
570 MinValue(f64),
571 MaxValue(f64),
572 Pattern(String),
573 EnumValues(Vec<String>),
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use oxirs_core::model::Literal;
580
581 const SAMPLE_SAMM_TTL: &str = r#"
582 @prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.0.0#> .
583 @prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.0.0#> .
584 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
585 @prefix phys: <http://oxirs.org/physics#> .
586 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
587
588 phys:RigidBody a samm:Aspect ;
589 rdfs:label "Rigid Body" ;
590 samm:description "A rigid body with physical properties" .
591
592 phys:mass a samm:Property ;
593 rdfs:label "mass" ;
594 samm:dataType xsd:double ;
595 samm:characteristic phys:MassCharacteristic .
596
597 phys:MassCharacteristic a samm-c:Measurement ;
598 samm:unit "kg" ;
599 samm-c:minValue "0.0"^^xsd:double .
600
601 phys:position a samm:Property ;
602 rdfs:label "position" ;
603 samm:dataType phys:Vector3D .
604 "#;
605
606 #[tokio::test]
607 async fn test_parse_samm_string() {
608 let parser = SammParser::new().expect("Failed to create parser");
609
610 let model = parser
611 .parse_samm_string(SAMPLE_SAMM_TTL)
612 .await
613 .expect("Failed to parse SAMM");
614
615 assert!(!model.entities.is_empty(), "Should have entities");
616 assert!(!model.properties.is_empty(), "Should have properties");
617 }
618
619 #[tokio::test]
620 async fn test_extract_entity_types() {
621 let parser = SammParser::new().expect("Failed to create parser");
622 let model = parser
623 .parse_samm_string(SAMPLE_SAMM_TTL)
624 .await
625 .expect("Failed to parse");
626
627 assert!(
629 !model.entities.is_empty(),
630 "Should have at least one entity"
631 );
632
633 let rigid_body = model.entities.iter().find(|e| e.name == "Rigid Body");
634 assert!(rigid_body.is_some(), "Should have RigidBody entity");
635 }
636
637 #[tokio::test]
638 async fn test_extract_properties() {
639 let parser = SammParser::new().expect("Failed to create parser");
640 let model = parser
641 .parse_samm_string(SAMPLE_SAMM_TTL)
642 .await
643 .expect("Failed to parse");
644
645 assert!(!model.properties.is_empty(), "Should have properties");
647
648 let mass_prop = model.properties.iter().find(|p| p.name == "mass");
649 assert!(mass_prop.is_some(), "Should have mass property");
650
651 }
654
655 #[tokio::test]
656 async fn test_extract_constraints() {
657 let parser = SammParser::new().expect("Failed to create parser");
658 let model = parser
659 .parse_samm_string(SAMPLE_SAMM_TTL)
660 .await
661 .expect("Failed to parse");
662
663 assert!(!model.properties.is_empty() || !model.entities.is_empty());
670 }
671
672 #[tokio::test]
673 async fn test_generate_sparql_query() {
674 let parser = SammParser::new().expect("Failed to create parser");
675 let mass_uri = NamedNode::new("http://oxirs.org/physics#mass").expect("Invalid URI");
676 let pos_uri = NamedNode::new("http://oxirs.org/physics#position").expect("Invalid URI");
677
678 let entity = EntityType {
679 uri: NamedNode::new("http://oxirs.org/physics#RigidBody").expect("Invalid URI"),
680 name: "RigidBody".to_string(),
681 description: Some("A rigid body".to_string()),
682 properties: vec![mass_uri, pos_uri],
683 };
684
685 let query = parser.generate_sparql_query(&entity);
686
687 assert!(query.contains("SELECT"), "Query should contain SELECT");
688 assert!(query.contains("?entity"), "Query should contain ?entity");
689 assert!(
690 query.contains("RigidBody"),
691 "Query should contain entity name"
692 );
693 }
694
695 #[test]
696 fn test_validate_data() {
697 let parser = SammParser::new().expect("Failed to create parser");
698
699 let mut values = HashMap::new();
700 values.insert("mass".to_string(), 10.0);
701 values.insert("temperature".to_string(), 300.0);
702
703 let mass_prop = NamedNode::new("http://oxirs.org/physics#mass").expect("Invalid URI");
704 let temp_prop =
705 NamedNode::new("http://oxirs.org/physics#temperature").expect("Invalid URI");
706
707 let constraints = vec![
708 ConstraintDefinition {
709 property: mass_prop.clone(),
710 constraint_type: ConstraintType::MinValue(0.0),
711 value: Term::Literal(Literal::new("0.0")),
712 },
713 ConstraintDefinition {
714 property: temp_prop.clone(),
715 constraint_type: ConstraintType::Range(0.0, 1000.0),
716 value: Term::Literal(Literal::new("0.0")),
717 },
718 ];
719
720 assert!(parser.validate_data(&values, &constraints).is_ok());
722
723 let mut invalid_values = values.clone();
725 invalid_values.insert("mass".to_string(), -1.0);
726 assert!(parser.validate_data(&invalid_values, &constraints).is_err());
727
728 let mut invalid_values2 = values.clone();
730 invalid_values2.insert("temperature".to_string(), 1500.0);
731 assert!(parser
732 .validate_data(&invalid_values2, &constraints)
733 .is_err());
734 }
735
736 #[test]
737 fn test_constraint_types() {
738 let min_constraint = ConstraintType::MinValue(0.0);
739 let max_constraint = ConstraintType::MaxValue(100.0);
740 let range_constraint = ConstraintType::Range(0.0, 100.0);
741
742 assert_eq!(min_constraint, ConstraintType::MinValue(0.0));
743 assert_eq!(max_constraint, ConstraintType::MaxValue(100.0));
744 assert_eq!(range_constraint, ConstraintType::Range(0.0, 100.0));
745 }
746
747 #[test]
748 fn test_aspect_model_serialization() {
749 let model = AspectModel {
750 entities: Vec::new(),
751 properties: Vec::new(),
752 relationships: Vec::new(),
753 constraints: Vec::new(),
754 };
755
756 let json = serde_json::to_string(&model).expect("Failed to serialize");
757
758 assert!(json.contains("entities"));
760 assert!(json.contains("properties"));
761 assert!(json.contains("relationships"));
762 assert!(json.contains("constraints"));
763 }
764}