1use crate::graph::Graph;
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4
5#[derive(Debug, Clone)]
6pub enum SbvrError {
7 SerializationError(String),
8 UnsupportedConstruct(String),
9}
10
11impl std::fmt::Display for SbvrError {
12 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 match self {
14 SbvrError::SerializationError(msg) => write!(f, "SBVR serialization error: {}", msg),
15 SbvrError::UnsupportedConstruct(msg) => write!(f, "Unsupported construct: {}", msg),
16 }
17 }
18}
19
20impl std::error::Error for SbvrError {}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SbvrTerm {
24 pub id: String,
25 pub name: String,
26 pub term_type: TermType,
27 pub definition: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum TermType {
32 GeneralConcept,
33 IndividualConcept,
34 VerbConcept,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SbvrFactType {
39 pub id: String,
40 pub subject: String,
41 pub verb: String,
42 pub object: String,
43 #[serde(default)]
44 pub destination: Option<String>,
45 #[serde(default = "SbvrFactType::default_schema_version")]
46 pub schema_version: String,
47}
48
49impl SbvrFactType {
50 pub fn default_schema_version() -> String {
51 "2.0".to_string()
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SbvrBusinessRule {
57 pub id: String,
58 pub name: String,
59 pub rule_type: RuleType,
60 pub expression: String,
61 pub severity: String,
62 #[serde(default)]
63 pub priority: Option<u8>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub enum RuleType {
68 Obligation,
69 Prohibition,
70 Permission,
71 Derivation,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SbvrModel {
76 pub vocabulary: Vec<SbvrTerm>,
77 pub facts: Vec<SbvrFactType>,
78 pub rules: Vec<SbvrBusinessRule>,
79}
80
81impl SbvrModel {
82 pub fn new() -> Self {
83 Self {
84 vocabulary: Vec::new(),
85 facts: Vec::new(),
86 rules: Vec::new(),
87 }
88 }
89
90 pub fn from_graph(graph: &Graph) -> Result<Self, SbvrError> {
91 let mut model = Self::new();
92 let mut relation_predicates: HashSet<String> = HashSet::new();
93
94 for entity in graph.all_entities() {
95 model.vocabulary.push(SbvrTerm {
96 id: entity.id().to_string(),
97 name: entity.name().to_string(),
98 term_type: TermType::GeneralConcept,
99 definition: Some(format!("Entity: {}", entity.name())),
100 });
101 }
102
103 for role in graph.all_roles() {
104 model.vocabulary.push(SbvrTerm {
105 id: role.id().to_string(),
106 name: role.name().to_string(),
107 term_type: TermType::GeneralConcept,
108 definition: Some(format!("Role: {}", role.name())),
109 });
110 }
111
112 for resource in graph.all_resources() {
113 model.vocabulary.push(SbvrTerm {
114 id: resource.id().to_string(),
115 name: resource.name().to_string(),
116 term_type: TermType::IndividualConcept,
117 definition: Some(format!(
118 "Resource: {} ({})",
119 resource.name(),
120 resource.unit()
121 )),
122 });
123 }
124
125 for pattern in graph.all_patterns() {
126 model.vocabulary.push(SbvrTerm {
127 id: pattern.id().to_string(),
128 name: pattern.name().to_string(),
129 term_type: TermType::IndividualConcept,
130 definition: Some(format!(
131 "Pattern '{}' matches {}",
132 pattern.name(),
133 pattern.regex()
134 )),
135 });
136 }
137
138 model.vocabulary.push(SbvrTerm {
139 id: "verb:transfers".to_string(),
140 name: "transfers".to_string(),
141 term_type: TermType::VerbConcept,
142 definition: Some("Transfer of resource between entities".to_string()),
143 });
144
145 for relation in graph.all_relations() {
146 if relation_predicates.insert(relation.predicate().to_string()) {
147 model.vocabulary.push(SbvrTerm {
148 id: format!("verb:{}", relation.predicate()),
149 name: relation.predicate().to_string(),
150 term_type: TermType::VerbConcept,
151 definition: Some(format!(
152 "Fact type predicate '{}' connecting declared roles",
153 relation.predicate()
154 )),
155 });
156 }
157
158 model.facts.push(SbvrFactType {
159 id: relation.id().to_string(),
160 subject: graph
161 .get_role(relation.subject_role())
162 .map(|role| role.name().to_string())
163 .unwrap_or_else(|| relation.subject_role().to_string()),
164 verb: relation.predicate().to_string(),
165 object: graph
166 .get_role(relation.object_role())
167 .map(|role| role.name().to_string())
168 .unwrap_or_else(|| relation.object_role().to_string()),
169 destination: relation.via_flow().map(|id| {
170 graph
171 .get_resource(id)
172 .map(|resource| resource.name().to_string())
173 .unwrap_or_else(|| id.to_string())
174 }),
175 schema_version: SbvrFactType::default_schema_version(),
176 });
177 }
178
179 for flow in graph.all_flows() {
180 model.facts.push(SbvrFactType {
181 id: flow.id().to_string(),
182 subject: flow.from_id().to_string(),
183 verb: "transfers".to_string(),
184 object: flow.resource_id().to_string(),
185 destination: Some(flow.to_id().to_string()),
186 schema_version: SbvrFactType::default_schema_version(),
187 });
188 }
189
190 Ok(model)
191 }
192
193 pub fn to_xmi(&self) -> Result<String, SbvrError> {
194 let mut xmi = String::new();
195
196 xmi.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
197 xmi.push_str("<xmi:XMI xmlns:xmi=\"http://www.omg.org/XMI\" xmlns:sbvr=\"http://www.omg.org/spec/SBVR/20080801\">\n");
198 xmi.push_str(" <sbvr:Vocabulary name=\"SEA_Model\">\n");
199
200 for term in &self.vocabulary {
201 match term.term_type {
202 TermType::GeneralConcept => {
203 xmi.push_str(&format!(
204 " <sbvr:GeneralConcept id=\"{}\" name=\"{}\">\n",
205 Self::escape_xml(&term.id),
206 Self::escape_xml(&term.name)
207 ));
208 if let Some(def) = &term.definition {
209 xmi.push_str(&format!(
210 " <sbvr:Definition>{}</sbvr:Definition>\n",
211 Self::escape_xml(def)
212 ));
213 }
214 xmi.push_str(" </sbvr:GeneralConcept>\n");
215 }
216 TermType::IndividualConcept => {
217 xmi.push_str(&format!(
218 " <sbvr:IndividualConcept id=\"{}\" name=\"{}\">\n",
219 Self::escape_xml(&term.id),
220 Self::escape_xml(&term.name)
221 ));
222 if let Some(def) = &term.definition {
223 xmi.push_str(&format!(
224 " <sbvr:Definition>{}</sbvr:Definition>\n",
225 Self::escape_xml(def)
226 ));
227 }
228 xmi.push_str(" </sbvr:IndividualConcept>\n");
229 }
230 TermType::VerbConcept => {
231 xmi.push_str(&format!(
232 " <sbvr:VerbConcept id=\"{}\" name=\"{}\">\n",
233 Self::escape_xml(&term.id),
234 Self::escape_xml(&term.name)
235 ));
236 if let Some(def) = &term.definition {
237 xmi.push_str(&format!(
238 " <sbvr:Definition>{}</sbvr:Definition>\n",
239 Self::escape_xml(def)
240 ));
241 }
242 xmi.push_str(" </sbvr:VerbConcept>\n");
243 }
244 }
245 }
246
247 for fact in &self.facts {
248 xmi.push_str(&format!(
249 " <sbvr:FactType id=\"{}\">\n",
250 Self::escape_xml(&fact.id)
251 ));
252 xmi.push_str(&format!(
253 " <sbvr:SchemaVersion>{}</sbvr:SchemaVersion>\n",
254 Self::escape_xml(&fact.schema_version)
255 ));
256 xmi.push_str(&format!(
257 " <sbvr:Subject>{}</sbvr:Subject>\n",
258 Self::escape_xml(&fact.subject)
259 ));
260 xmi.push_str(&format!(
261 " <sbvr:Verb>{}</sbvr:Verb>\n",
262 Self::escape_xml(&fact.verb)
263 ));
264 xmi.push_str(&format!(
265 " <sbvr:Object>{}</sbvr:Object>\n",
266 Self::escape_xml(&fact.object)
267 ));
268 if let Some(dest) = &fact.destination {
269 xmi.push_str(&format!(
270 " <sbvr:Destination>{}</sbvr:Destination>\n",
271 Self::escape_xml(dest)
272 ));
273 }
274 xmi.push_str(" </sbvr:FactType>\n");
275 }
276
277 for rule in &self.rules {
278 let rule_element = match rule.rule_type {
279 RuleType::Obligation => "Obligation",
280 RuleType::Prohibition => "Prohibition",
281 RuleType::Permission => "Permission",
282 RuleType::Derivation => "Derivation",
283 };
284
285 xmi.push_str(&format!(
286 " <sbvr:{} id=\"{}\" name=\"{}\">\n",
287 rule_element,
288 Self::escape_xml(&rule.id),
289 Self::escape_xml(&rule.name)
290 ));
291 xmi.push_str(&format!(
292 " <sbvr:Expression>{}</sbvr:Expression>\n",
293 Self::escape_xml(&rule.expression)
294 ));
295 xmi.push_str(&format!(
296 " <sbvr:Severity>{}</sbvr:Severity>\n",
297 Self::escape_xml(&rule.severity)
298 ));
299 if let Some(p) = rule.priority {
300 xmi.push_str(&format!(" <sbvr:Priority>{}</sbvr:Priority>\n", p));
301 }
302 xmi.push_str(&format!(" </sbvr:{}>\n", rule_element));
303 }
304
305 xmi.push_str(" </sbvr:Vocabulary>\n");
306 xmi.push_str("</xmi:XMI>\n");
307
308 Ok(xmi)
309 }
310
311 fn escape_xml(s: &str) -> String {
312 s.replace('&', "&")
313 .replace('<', "<")
314 .replace('>', ">")
315 .replace('"', """)
316 .replace('\'', "'")
317 }
318
319 pub fn from_xmi(xmi: &str) -> Result<Self, SbvrError> {
321 let doc = roxmltree::Document::parse(xmi)
322 .map_err(|e| SbvrError::SerializationError(format!("Failed to parse XMI: {}", e)))?;
323
324 let mut model = SbvrModel::new();
325
326 for node in doc.descendants() {
328 if node.has_tag_name("GeneralConcept") {
329 let id = node.attribute("id").unwrap_or_default().to_string();
330 let name = node.attribute("name").unwrap_or_default().to_string();
331 let mut definition = None;
332 for child in node.children() {
333 if child.has_tag_name("Definition") {
334 definition = Some(child.text().unwrap_or_default().to_string());
335 }
336 }
337 model.vocabulary.push(SbvrTerm {
338 id,
339 name,
340 term_type: TermType::GeneralConcept,
341 definition,
342 });
343 }
344
345 if node.has_tag_name("IndividualConcept") {
346 let id = node.attribute("id").unwrap_or_default().to_string();
347 let name = node.attribute("name").unwrap_or_default().to_string();
348 let mut definition = None;
349 for child in node.children() {
350 if child.has_tag_name("Definition") {
351 definition = Some(child.text().unwrap_or_default().to_string());
352 }
353 }
354 model.vocabulary.push(SbvrTerm {
355 id,
356 name,
357 term_type: TermType::IndividualConcept,
358 definition,
359 });
360 }
361
362 if node.has_tag_name("VerbConcept") {
363 let id = node.attribute("id").unwrap_or_default().to_string();
364 let name = node.attribute("name").unwrap_or_default().to_string();
365 let mut definition = None;
366 for child in node.children() {
367 if child.has_tag_name("Definition") {
368 definition = Some(child.text().unwrap_or_default().to_string());
369 }
370 }
371 model.vocabulary.push(SbvrTerm {
372 id,
373 name,
374 term_type: TermType::VerbConcept,
375 definition,
376 });
377 }
378
379 if node.has_tag_name("FactType") {
380 let id = node.attribute("id").unwrap_or_default().to_string();
381 let mut subject = String::new();
382 let mut verb = String::new();
383 let mut object = String::new();
384 let mut destination = None;
385 let mut schema_version = SbvrFactType::default_schema_version();
386
387 for child in node.children() {
388 if child.has_tag_name("SchemaVersion") {
389 schema_version = child.text().unwrap_or_default().to_string();
390 }
391 if child.has_tag_name("Subject") {
392 subject = child.text().unwrap_or_default().to_string();
393 }
394 if child.has_tag_name("Verb") {
395 verb = child.text().unwrap_or_default().to_string();
396 }
397 if child.has_tag_name("Object") {
398 object = child.text().unwrap_or_default().to_string();
399 }
400 if child.has_tag_name("Destination") {
401 destination = Some(child.text().unwrap_or_default().to_string());
402 }
403 }
404
405 model.facts.push(SbvrFactType {
406 id,
407 subject,
408 verb,
409 object,
410 destination,
411 schema_version,
412 });
413 }
414
415 if node.has_tag_name("Obligation")
417 || node.has_tag_name("Prohibition")
418 || node.has_tag_name("Permission")
419 || node.has_tag_name("Derivation")
420 {
421 let id = node.attribute("id").unwrap_or_default().to_string();
422 let name = node.attribute("name").unwrap_or_default().to_string();
423 let kind = if node.has_tag_name("Obligation") {
424 RuleType::Obligation
425 } else if node.has_tag_name("Prohibition") {
426 RuleType::Prohibition
427 } else if node.has_tag_name("Permission") {
428 RuleType::Permission
429 } else {
430 RuleType::Derivation
431 };
432 let mut expression = String::new();
433 let mut severity = String::from("Info");
434 let mut parsed_priority: Option<u8> = None;
435 for child in node.children() {
436 if child.has_tag_name("Expression") {
437 expression = child.text().unwrap_or_default().to_string();
438 }
439 if child.has_tag_name("Severity") {
440 severity = child.text().unwrap_or_default().to_string();
441 }
442 if child.has_tag_name("Priority") {
443 if let Some(text) = child.text() {
444 if let Ok(value) = text.trim().parse::<u8>() {
445 parsed_priority = Some(value);
446 }
447 }
448 }
449 }
450 let mut rule = SbvrBusinessRule {
451 id,
452 name,
453 rule_type: kind,
454 expression,
455 severity,
456 priority: parsed_priority,
457 };
458 if rule.priority.is_none() {
459 rule.priority = Some(match rule.rule_type {
460 RuleType::Obligation => 5,
461 RuleType::Prohibition => 5,
462 RuleType::Permission => 1,
463 RuleType::Derivation => 3,
464 });
465 }
466 model.rules.push(rule);
467 }
468 }
469
470 Ok(model)
471 }
472
473 pub fn to_graph(&self) -> Result<crate::graph::Graph, SbvrError> {
475 use crate::graph::Graph;
476 use crate::primitives::{Entity, Flow, Resource};
477 use crate::units::unit_from_string;
478 use rust_decimal::Decimal;
479
480 let mut graph = Graph::new();
481
482 for term in &self.vocabulary {
484 match term.term_type {
485 TermType::GeneralConcept => {
486 let entity =
487 Entity::new_with_namespace(term.name.clone(), "default".to_string());
488 graph
489 .add_entity(entity)
490 .map_err(SbvrError::SerializationError)?;
491 }
492 TermType::IndividualConcept => {
493 let unit_symbol = term.definition.as_deref().and_then(|def| {
494 if let Some(open) = def.rfind('(') {
495 if let Some(close_offset) = def[open..].find(')') {
496 let close = open + close_offset;
497 let candidate = def[open + 1..close].trim();
498 if !candidate.is_empty() {
499 return Some(candidate.to_string());
500 }
501 }
502 }
503 None
504 });
505 let unit_symbol = unit_symbol.unwrap_or_else(|| {
506 "units".to_string()
509 });
510 let res = Resource::new_with_namespace(
511 term.name.clone(),
512 unit_from_string(&unit_symbol),
513 "default".to_string(),
514 );
515 graph
516 .add_resource(res)
517 .map_err(SbvrError::SerializationError)?;
518 }
519 TermType::VerbConcept => {
520 }
522 }
523 }
524
525 for fact in &self.facts {
527 let subject_name = self
529 .vocabulary
530 .iter()
531 .find(|t| t.id == fact.subject)
532 .map(|t| t.name.clone())
533 .unwrap_or(fact.subject.clone());
534
535 let object_name = self
536 .vocabulary
537 .iter()
538 .find(|t| t.id == fact.object)
539 .map(|t| t.name.clone())
540 .unwrap_or(fact.object.clone());
541
542 let destination_name = fact
543 .destination
544 .clone()
545 .and_then(|d| {
546 self.vocabulary
547 .iter()
548 .find(|t| t.id == d)
549 .map(|t| t.name.clone())
550 })
551 .unwrap_or_default();
552
553 let subject_id = graph.find_entity_by_name(&subject_name).ok_or_else(|| {
554 SbvrError::UnsupportedConstruct(format!("Unknown subject entity: {}", subject_name))
555 })?;
556
557 let destination_id = graph
558 .find_entity_by_name(&destination_name)
559 .ok_or_else(|| {
560 SbvrError::UnsupportedConstruct(format!(
561 "Unknown destination entity: {}",
562 destination_name
563 ))
564 })?;
565
566 let resource_id = graph.find_resource_by_name(&object_name).ok_or_else(|| {
567 SbvrError::UnsupportedConstruct(format!("Unknown resource: {}", object_name))
568 })?;
569
570 let quantity = Decimal::from(1);
573
574 let flow = Flow::new(resource_id, subject_id, destination_id, quantity);
575 graph
576 .add_flow(flow)
577 .map_err(SbvrError::SerializationError)?;
578 }
579
580 for rule in &self.rules {
582 let expr = crate::parser::parse_expression_from_str(rule.expression.as_str()).map_err(
584 |e| {
585 SbvrError::SerializationError(format!("Failed to parse rule expression: {}", e))
586 },
587 )?;
588
589 let mut policy = crate::policy::Policy::new_with_namespace(
590 rule.name.clone(),
591 "default".to_string(),
592 expr,
593 );
594
595 match rule.rule_type {
597 RuleType::Obligation => {
598 policy = policy.with_modality(crate::policy::PolicyModality::Obligation);
599 policy = policy.with_kind(crate::policy::PolicyKind::Constraint);
600 }
601 RuleType::Prohibition => {
602 policy = policy.with_modality(crate::policy::PolicyModality::Prohibition);
603 policy = policy.with_kind(crate::policy::PolicyKind::Constraint);
604 }
605 RuleType::Permission => {
606 policy = policy.with_modality(crate::policy::PolicyModality::Permission);
607 policy = policy.with_kind(crate::policy::PolicyKind::Constraint);
608 }
609 RuleType::Derivation => {
610 policy = policy.with_modality(crate::policy::PolicyModality::Permission);
611 policy = policy.with_kind(crate::policy::PolicyKind::Derivation);
612 }
613 }
614
615 if let Some(p) = rule.priority {
617 policy = policy.with_priority(p as i32);
618 } else {
619 let default_p = match rule.rule_type {
621 RuleType::Obligation => 5,
622 RuleType::Prohibition => 5,
623 RuleType::Permission => 1,
624 RuleType::Derivation => 3,
625 };
626 policy = policy.with_priority(default_p);
627 }
628
629 graph
630 .add_policy(policy)
631 .map_err(SbvrError::SerializationError)?;
632 }
633
634 Ok(graph)
635 }
636}
637
638impl Default for SbvrModel {
639 fn default() -> Self {
640 Self::new()
641 }
642}
643
644impl Graph {
645 pub fn export_sbvr(&self) -> Result<String, SbvrError> {
646 let model = SbvrModel::from_graph(self)?;
647 model.to_xmi()
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::primitives::{Entity, Flow, Resource};
655 use rust_decimal::Decimal;
656
657 #[test]
658 fn test_sbvr_model_creation() {
659 let model = SbvrModel::new();
660 assert_eq!(model.vocabulary.len(), 0);
661 assert_eq!(model.facts.len(), 0);
662 assert_eq!(model.rules.len(), 0);
663 }
664
665 #[test]
666 fn test_export_to_sbvr() {
667 let mut graph = Graph::new();
668
669 let entity1 = Entity::new_with_namespace("Supplier", "supply_chain");
670 let entity2 = Entity::new_with_namespace("Manufacturer", "supply_chain");
671 let resource = Resource::new_with_namespace(
672 "Parts",
673 crate::units::unit_from_string("kg"),
674 "supply_chain",
675 );
676
677 let entity1_id = entity1.id().clone();
678 let entity2_id = entity2.id().clone();
679 let resource_id = resource.id().clone();
680
681 graph.add_entity(entity1).unwrap();
682 graph.add_entity(entity2).unwrap();
683 graph.add_resource(resource).unwrap();
684
685 #[allow(deprecated)]
686 let flow = Flow::new(resource_id, entity1_id, entity2_id, Decimal::new(100, 0));
687 graph.add_flow(flow).unwrap();
688
689 let sbvr_xml = graph.export_sbvr().unwrap();
690
691 assert!(sbvr_xml.contains("<sbvr:FactType"));
692 assert!(sbvr_xml.contains("<sbvr:GeneralConcept"));
693 assert!(sbvr_xml.contains("Supplier"));
694 assert!(sbvr_xml.contains("Manufacturer"));
695 }
696
697 #[test]
698 fn test_xml_escaping() {
699 assert_eq!(SbvrModel::escape_xml("A&B"), "A&B");
700 assert_eq!(SbvrModel::escape_xml("<tag>"), "<tag>");
701 assert_eq!(SbvrModel::escape_xml("\"quote\""), ""quote"");
702 }
703
704 #[test]
705 fn test_sbvr_rule_to_policy() {
706 let mut model = SbvrModel::new();
707
708 model.vocabulary.push(SbvrTerm {
710 id: "e1".to_string(),
711 name: "Warehouse".to_string(),
712 term_type: TermType::GeneralConcept,
713 definition: None,
714 });
715 model.vocabulary.push(SbvrTerm {
716 id: "e2".to_string(),
717 name: "Factory".to_string(),
718 term_type: TermType::GeneralConcept,
719 definition: None,
720 });
721 model.vocabulary.push(SbvrTerm {
722 id: "r1".to_string(),
723 name: "Cameras".to_string(),
724 term_type: TermType::IndividualConcept,
725 definition: None,
726 });
727
728 model.rules.push(SbvrBusinessRule {
729 id: "rule1".to_string(),
730 name: "MustHavePositiveQuantity".to_string(),
731 rule_type: RuleType::Obligation,
732 expression: "forall f in flows: (f.quantity > 0)".to_string(),
733 severity: "Info".to_string(),
734 priority: None,
735 });
736
737 let graph = model.to_graph().expect("SBVR to Graph conversion failed");
738 assert_eq!(graph.policy_count(), 1);
739 let policy = graph.all_policies().into_iter().next().unwrap();
740 assert_eq!(policy.name, "MustHavePositiveQuantity");
741 assert_eq!(policy.priority, 5); assert_eq!(policy.modality, crate::policy::PolicyModality::Obligation);
743 }
744}