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) => {
480 if value < *min {
481 return Err(PhysicsError::ConstraintViolation(format!(
482 "Property {} value {} is less than minimum {}",
483 prop_name, value, min
484 )));
485 }
486 }
487 ConstraintType::MaxValue(max) => {
488 if value > *max {
489 return Err(PhysicsError::ConstraintViolation(format!(
490 "Property {} value {} is greater than maximum {}",
491 prop_name, value, max
492 )));
493 }
494 }
495 ConstraintType::Range(min, max) => {
496 if value < *min || value > *max {
497 return Err(PhysicsError::ConstraintViolation(format!(
498 "Property {} value {} is outside range [{}, {}]",
499 prop_name, value, min, max
500 )));
501 }
502 }
503 _ => {}
504 }
505 }
506 }
507
508 Ok(())
509 }
510}
511
512impl Default for SammParser {
513 fn default() -> Self {
514 Self::new().expect("Failed to create default SAMM parser")
515 }
516}
517
518#[derive(Debug, Clone, Serialize)]
520pub struct AspectModel {
521 pub entities: Vec<EntityType>,
522 pub properties: Vec<PropertyDefinition>,
523 pub relationships: Vec<RelationshipDefinition>,
524 pub constraints: Vec<ConstraintDefinition>,
525}
526
527#[derive(Debug, Clone, Serialize)]
529pub struct EntityType {
530 #[serde(skip)]
531 pub uri: NamedNode,
532 pub name: String,
533 pub description: Option<String>,
534 #[serde(skip)]
535 pub properties: Vec<NamedNode>,
536}
537
538#[derive(Debug, Clone, Serialize)]
540pub struct PropertyDefinition {
541 #[serde(skip)]
542 pub uri: NamedNode,
543 pub name: String,
544 #[serde(skip)]
545 pub datatype: NamedNode,
546 pub unit: Option<String>,
547 pub optional: bool,
548}
549
550#[derive(Debug, Clone, Serialize)]
552pub struct RelationshipDefinition {
553 #[serde(skip)]
554 pub source: NamedNode,
555 #[serde(skip)]
556 pub predicate: NamedNode,
557 #[serde(skip)]
558 pub target: NamedNode,
559 pub name: String,
560}
561
562#[derive(Debug, Clone, Serialize)]
564pub struct ConstraintDefinition {
565 #[serde(skip)]
566 pub property: NamedNode,
567 pub constraint_type: ConstraintType,
568 #[serde(skip)]
569 pub value: Term,
570}
571
572#[derive(Debug, Clone, Serialize, PartialEq)]
574pub enum ConstraintType {
575 Range(f64, f64),
576 MinValue(f64),
577 MaxValue(f64),
578 Pattern(String),
579 EnumValues(Vec<String>),
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use oxirs_core::model::Literal;
586
587 const SAMPLE_SAMM_TTL: &str = r#"
588 @prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.0.0#> .
589 @prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.0.0#> .
590 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
591 @prefix phys: <http://oxirs.org/physics#> .
592 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
593
594 phys:RigidBody a samm:Aspect ;
595 rdfs:label "Rigid Body" ;
596 samm:description "A rigid body with physical properties" .
597
598 phys:mass a samm:Property ;
599 rdfs:label "mass" ;
600 samm:dataType xsd:double ;
601 samm:characteristic phys:MassCharacteristic .
602
603 phys:MassCharacteristic a samm-c:Measurement ;
604 samm:unit "kg" ;
605 samm-c:minValue "0.0"^^xsd:double .
606
607 phys:position a samm:Property ;
608 rdfs:label "position" ;
609 samm:dataType phys:Vector3D .
610 "#;
611
612 #[tokio::test]
613 async fn test_parse_samm_string() {
614 let parser = SammParser::new().expect("Failed to create parser");
615
616 let model = parser
617 .parse_samm_string(SAMPLE_SAMM_TTL)
618 .await
619 .expect("Failed to parse SAMM");
620
621 assert!(!model.entities.is_empty(), "Should have entities");
622 assert!(!model.properties.is_empty(), "Should have properties");
623 }
624
625 #[tokio::test]
626 async fn test_extract_entity_types() {
627 let parser = SammParser::new().expect("Failed to create parser");
628 let model = parser
629 .parse_samm_string(SAMPLE_SAMM_TTL)
630 .await
631 .expect("Failed to parse");
632
633 assert!(
635 !model.entities.is_empty(),
636 "Should have at least one entity"
637 );
638
639 let rigid_body = model.entities.iter().find(|e| e.name == "Rigid Body");
640 assert!(rigid_body.is_some(), "Should have RigidBody entity");
641 }
642
643 #[tokio::test]
644 async fn test_extract_properties() {
645 let parser = SammParser::new().expect("Failed to create parser");
646 let model = parser
647 .parse_samm_string(SAMPLE_SAMM_TTL)
648 .await
649 .expect("Failed to parse");
650
651 assert!(!model.properties.is_empty(), "Should have properties");
653
654 let mass_prop = model.properties.iter().find(|p| p.name == "mass");
655 assert!(mass_prop.is_some(), "Should have mass property");
656
657 }
660
661 #[tokio::test]
662 async fn test_extract_constraints() {
663 let parser = SammParser::new().expect("Failed to create parser");
664 let model = parser
665 .parse_samm_string(SAMPLE_SAMM_TTL)
666 .await
667 .expect("Failed to parse");
668
669 assert!(!model.properties.is_empty() || !model.entities.is_empty());
676 }
677
678 #[tokio::test]
679 async fn test_generate_sparql_query() {
680 let parser = SammParser::new().expect("Failed to create parser");
681 let mass_uri = NamedNode::new("http://oxirs.org/physics#mass").expect("Invalid URI");
682 let pos_uri = NamedNode::new("http://oxirs.org/physics#position").expect("Invalid URI");
683
684 let entity = EntityType {
685 uri: NamedNode::new("http://oxirs.org/physics#RigidBody").expect("Invalid URI"),
686 name: "RigidBody".to_string(),
687 description: Some("A rigid body".to_string()),
688 properties: vec![mass_uri, pos_uri],
689 };
690
691 let query = parser.generate_sparql_query(&entity);
692
693 assert!(query.contains("SELECT"), "Query should contain SELECT");
694 assert!(query.contains("?entity"), "Query should contain ?entity");
695 assert!(
696 query.contains("RigidBody"),
697 "Query should contain entity name"
698 );
699 }
700
701 #[test]
702 fn test_validate_data() {
703 let parser = SammParser::new().expect("Failed to create parser");
704
705 let mut values = HashMap::new();
706 values.insert("mass".to_string(), 10.0);
707 values.insert("temperature".to_string(), 300.0);
708
709 let mass_prop = NamedNode::new("http://oxirs.org/physics#mass").expect("Invalid URI");
710 let temp_prop =
711 NamedNode::new("http://oxirs.org/physics#temperature").expect("Invalid URI");
712
713 let constraints = vec![
714 ConstraintDefinition {
715 property: mass_prop.clone(),
716 constraint_type: ConstraintType::MinValue(0.0),
717 value: Term::Literal(Literal::new("0.0")),
718 },
719 ConstraintDefinition {
720 property: temp_prop.clone(),
721 constraint_type: ConstraintType::Range(0.0, 1000.0),
722 value: Term::Literal(Literal::new("0.0")),
723 },
724 ];
725
726 assert!(parser.validate_data(&values, &constraints).is_ok());
728
729 let mut invalid_values = values.clone();
731 invalid_values.insert("mass".to_string(), -1.0);
732 assert!(parser.validate_data(&invalid_values, &constraints).is_err());
733
734 let mut invalid_values2 = values.clone();
736 invalid_values2.insert("temperature".to_string(), 1500.0);
737 assert!(parser
738 .validate_data(&invalid_values2, &constraints)
739 .is_err());
740 }
741
742 #[test]
743 fn test_constraint_types() {
744 let min_constraint = ConstraintType::MinValue(0.0);
745 let max_constraint = ConstraintType::MaxValue(100.0);
746 let range_constraint = ConstraintType::Range(0.0, 100.0);
747
748 assert_eq!(min_constraint, ConstraintType::MinValue(0.0));
749 assert_eq!(max_constraint, ConstraintType::MaxValue(100.0));
750 assert_eq!(range_constraint, ConstraintType::Range(0.0, 100.0));
751 }
752
753 #[test]
754 fn test_aspect_model_serialization() {
755 let model = AspectModel {
756 entities: Vec::new(),
757 properties: Vec::new(),
758 relationships: Vec::new(),
759 constraints: Vec::new(),
760 };
761
762 let json = serde_json::to_string(&model).expect("Failed to serialize");
763
764 assert!(json.contains("entities"));
766 assert!(json.contains("properties"));
767 assert!(json.contains("relationships"));
768 assert!(json.contains("constraints"));
769 }
770}