use crate::{Rule, RuleAtom, Term};
use anyhow::{anyhow, Context, Result};
use regex::Regex;
use std::collections::HashMap;
use tracing::{info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceFormat {
Jena,
Drools,
Clips,
}
impl SourceFormat {
pub fn name(&self) -> &str {
match self {
Self::Jena => "Apache Jena",
Self::Drools => "Drools DRL",
Self::Clips => "CLIPS",
}
}
}
#[derive(Debug, Clone)]
pub struct MigrationWarning {
pub message: String,
pub line: Option<usize>,
pub severity: WarningSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarningSeverity {
Info,
Warning,
Error,
}
#[derive(Debug)]
pub struct MigrationResult {
pub rules: Vec<Rule>,
pub warnings: Vec<MigrationWarning>,
pub source_format: SourceFormat,
pub original_count: usize,
pub migrated_count: usize,
}
impl MigrationResult {
pub fn generate_report(&self) -> String {
let mut report = String::new();
report.push_str("═══════════════════════════════════════════════════════════════\n");
report.push_str(&format!(
" Migration Report: {} → OxiRS\n",
self.source_format.name()
));
report.push_str("═══════════════════════════════════════════════════════════════\n\n");
report.push_str(&format!("Original rules: {}\n", self.original_count));
report.push_str(&format!("Migrated rules: {}\n", self.migrated_count));
report.push_str(&format!(
"Success rate: {:.1}%\n\n",
(self.migrated_count as f64 / self.original_count as f64) * 100.0
));
if !self.warnings.is_empty() {
report.push_str("Warnings:\n");
report.push_str("─────────────────────────────────────────────────────────────\n");
let mut errors = 0;
let mut warnings = 0;
let mut infos = 0;
for warning in &self.warnings {
let prefix = match warning.severity {
WarningSeverity::Error => {
errors += 1;
"ERROR"
}
WarningSeverity::Warning => {
warnings += 1;
"WARN"
}
WarningSeverity::Info => {
infos += 1;
"INFO"
}
};
if let Some(line) = warning.line {
report.push_str(&format!(
" [{}] Line {}: {}\n",
prefix, line, warning.message
));
} else {
report.push_str(&format!(" [{}] {}\n", prefix, warning.message));
}
}
report.push_str(&format!(
"\nSummary: {} errors, {} warnings, {} info\n",
errors, warnings, infos
));
}
report.push_str("\n═══════════════════════════════════════════════════════════════\n");
report
}
pub fn is_successful(&self) -> bool {
self.warnings
.iter()
.all(|w| w.severity != WarningSeverity::Error)
}
}
pub struct MigrationTool {
strict_mode: bool,
namespace_mappings: HashMap<String, String>,
}
impl Default for MigrationTool {
fn default() -> Self {
Self::new()
}
}
impl MigrationTool {
pub fn new() -> Self {
Self {
strict_mode: false,
namespace_mappings: HashMap::new(),
}
}
pub fn with_strict_mode(mut self, strict: bool) -> Self {
self.strict_mode = strict;
self
}
pub fn add_namespace_mapping(&mut self, from: &str, to: &str) {
self.namespace_mappings
.insert(from.to_string(), to.to_string());
}
pub fn migrate(&mut self, source: &str, format: SourceFormat) -> Result<MigrationResult> {
info!("Starting migration from {}", format.name());
let mut warnings = Vec::new();
let rules = match format {
SourceFormat::Jena => self.migrate_jena(source, &mut warnings)?,
SourceFormat::Drools => self.migrate_drools(source, &mut warnings)?,
SourceFormat::Clips => self.migrate_clips(source, &mut warnings)?,
};
let original_count = source
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#') && !trimmed.starts_with("//")
})
.count();
Ok(MigrationResult {
migrated_count: rules.len(),
rules,
warnings,
source_format: format,
original_count,
})
}
fn migrate_jena(
&mut self,
source: &str,
warnings: &mut Vec<MigrationWarning>,
) -> Result<Vec<Rule>> {
let mut rules = Vec::new();
let rule_regex = Regex::new(r"\[(\w+):\s*(.+?)\s*->\s*(.+?)\]")
.context("Failed to compile Jena rule regex")?;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if let Some(captures) = rule_regex.captures(trimmed) {
let rule_name = captures.get(1).expect("regex capture group 1").as_str();
let body_str = captures.get(2).expect("regex capture group 2").as_str();
let head_str = captures.get(3).expect("regex capture group 3").as_str();
match self.parse_jena_atoms(body_str, head_str, rule_name) {
Ok(rule) => rules.push(rule),
Err(e) => {
warnings.push(MigrationWarning {
message: format!("Failed to parse rule '{}': {}", rule_name, e),
line: Some(line_num + 1),
severity: if self.strict_mode {
WarningSeverity::Error
} else {
WarningSeverity::Warning
},
});
}
}
} else {
warnings.push(MigrationWarning {
message: format!("Could not parse Jena rule syntax: {}", trimmed),
line: Some(line_num + 1),
severity: WarningSeverity::Warning,
});
}
}
info!("Migrated {} Jena rules", rules.len());
Ok(rules)
}
fn parse_jena_atoms(&self, body_str: &str, head_str: &str, rule_name: &str) -> Result<Rule> {
let body = self.parse_jena_triples(body_str)?;
let head = self.parse_jena_triples(head_str)?;
Ok(Rule {
name: rule_name.to_string(),
body,
head,
})
}
fn parse_jena_triples(&self, patterns: &str) -> Result<Vec<RuleAtom>> {
let mut atoms = Vec::new();
for pattern in patterns.split(',') {
let pattern = pattern.trim();
if pattern.is_empty() {
continue;
}
let pattern = pattern.strip_prefix('(').unwrap_or(pattern);
let pattern = pattern.strip_suffix(')').unwrap_or(pattern);
let parts: Vec<&str> = pattern.split_whitespace().collect();
if parts.len() == 3 {
atoms.push(RuleAtom::Triple {
subject: self.parse_jena_term(parts[0])?,
predicate: self.parse_jena_term(parts[1])?,
object: self.parse_jena_term(parts[2])?,
});
} else {
return Err(anyhow!("Invalid triple pattern: {}", pattern));
}
}
Ok(atoms)
}
fn parse_jena_term(&self, term: &str) -> Result<Term> {
if let Some(var_name) = term.strip_prefix('?') {
Ok(Term::Variable(var_name.to_string()))
} else if term.starts_with('"') && term.ends_with('"') {
Ok(Term::Literal(term[1..term.len() - 1].to_string()))
} else {
let expanded = self.expand_namespace(term);
Ok(Term::Constant(expanded))
}
}
fn migrate_drools(
&mut self,
source: &str,
warnings: &mut Vec<MigrationWarning>,
) -> Result<Vec<Rule>> {
let mut rules = Vec::new();
let rule_regex = Regex::new(r#"rule\s+"([^"]+)"\s+when\s+(.+?)\s+then\s+(.+?)\s+end"#)
.context("Failed to compile Drools rule regex")?;
for (line_num, captures) in rule_regex.captures_iter(source).enumerate() {
let rule_name = captures.get(1).expect("regex capture group 1").as_str();
let when_clause = captures.get(2).expect("regex capture group 2").as_str();
let then_clause = captures.get(3).expect("regex capture group 3").as_str();
match self.parse_drools_rule(rule_name, when_clause, then_clause) {
Ok(rule) => {
if !rule.body.is_empty() || !rule.head.is_empty() {
rules.push(rule);
warnings.push(MigrationWarning {
message: format!(
"Drools rule '{}' migrated - complex DRL features (Java code, salience, etc.) not supported",
rule_name
),
line: Some(line_num + 1),
severity: WarningSeverity::Info,
});
} else {
warnings.push(MigrationWarning {
message: format!(
"Drools rule '{}' skipped - no parseable patterns found",
rule_name
),
line: Some(line_num + 1),
severity: WarningSeverity::Warning,
});
}
}
Err(e) => {
warnings.push(MigrationWarning {
message: format!("Failed to parse Drools rule '{}': {}", rule_name, e),
line: Some(line_num + 1),
severity: WarningSeverity::Error,
});
}
}
}
info!("Migrated {} Drools rules", rules.len());
Ok(rules)
}
fn parse_drools_rule(&self, name: &str, when_clause: &str, then_clause: &str) -> Result<Rule> {
let body = self.parse_drools_when_clause(when_clause)?;
let head = self.parse_drools_then_clause(then_clause)?;
if body.is_empty() && head.is_empty() {
warn!(
"Drools rule '{}' has no parseable conditions or actions",
name
);
}
Ok(Rule {
name: name.to_string(),
body,
head,
})
}
fn parse_drools_when_clause(&self, when_clause: &str) -> Result<Vec<RuleAtom>> {
let mut atoms = Vec::new();
let pattern_regex =
Regex::new(r"(\w+)\s*\((.*?)\)").context("Failed to compile Drools pattern regex")?;
for captures in pattern_regex.captures_iter(when_clause) {
let class_name = captures.get(1).expect("regex capture group 1").as_str();
let conditions = captures.get(2).expect("regex capture group 2").as_str();
for condition in conditions.split(',') {
let condition = condition.trim();
if condition.is_empty() {
continue;
}
if let Some((field, value)) = self.parse_drools_condition(condition) {
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("{}Instance", class_name)),
predicate: Term::Constant(field),
object: self.parse_drools_value(&value),
});
}
}
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("{}Instance", class_name)),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant(class_name.to_string()),
});
}
Ok(atoms)
}
fn parse_drools_then_clause(&self, then_clause: &str) -> Result<Vec<RuleAtom>> {
let mut atoms = Vec::new();
let insert_regex = Regex::new(r"insert\s*\(\s*new\s+(\w+)\s*\((.*?)\)\s*\)")
.context("Failed to compile Drools insert regex")?;
for captures in insert_regex.captures_iter(then_clause) {
let class_name = captures.get(1).expect("regex capture group 1").as_str();
let params = captures.get(2).expect("regex capture group 2").as_str();
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("new{}", class_name)),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant(class_name.to_string()),
});
for param in params.split(',') {
let param = param.trim();
if let Some((field, value)) = self.parse_drools_condition(param) {
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("new{}", class_name)),
predicate: Term::Constant(field),
object: self.parse_drools_value(&value),
});
}
}
}
let modify_regex = Regex::new(r"modify\s*\(\s*(\w+)\s*\)\s*\{([^}]+)\}")
.context("Failed to compile Drools modify regex")?;
let setter_regex =
Regex::new(r"set(\w+)\s*\(([^)]+)\)").context("Failed to compile setter regex")?;
for captures in modify_regex.captures_iter(then_clause) {
let var_name = captures.get(1).expect("regex capture group 1").as_str();
let modifications = captures.get(2).expect("regex capture group 2").as_str();
for setter_captures in setter_regex.captures_iter(modifications) {
let field_name = setter_captures
.get(1)
.expect("regex capture group 1")
.as_str();
let value = setter_captures
.get(2)
.expect("regex capture group 2")
.as_str()
.trim();
atoms.push(RuleAtom::Triple {
subject: Term::Variable(var_name.to_string()),
predicate: Term::Constant(field_name.to_string()),
object: self.parse_drools_value(value),
});
}
}
Ok(atoms)
}
fn parse_drools_condition(&self, condition: &str) -> Option<(String, String)> {
if let Some(eq_pos) = condition.find("==") {
let field = condition[..eq_pos].trim().to_string();
let value = condition[eq_pos + 2..].trim().to_string();
return Some((field, value));
}
if let Some(colon_pos) = condition.find(':') {
let field = condition[..colon_pos].trim().to_string();
let value = condition[colon_pos + 1..].trim().to_string();
return Some((field, value));
}
None
}
fn parse_drools_value(&self, value: &str) -> Term {
let value = value.trim();
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
return Term::Literal(value[1..value.len() - 1].to_string());
}
if let Some(var_name) = value.strip_prefix('$') {
return Term::Variable(var_name.to_string());
}
if value
.chars()
.all(|c| c.is_numeric() || c == '.' || c == '-')
{
Term::Literal(value.to_string())
} else {
Term::Constant(value.to_string())
}
}
fn migrate_clips(
&mut self,
source: &str,
warnings: &mut Vec<MigrationWarning>,
) -> Result<Vec<Rule>> {
let mut rules = Vec::new();
let rule_regex = Regex::new(r"\(defrule\s+(\S+)\s+(.+?)\s+=>\s+(.+?)\)")
.context("Failed to compile CLIPS rule regex")?;
for (line_num, captures) in rule_regex.captures_iter(source).enumerate() {
let rule_name = captures.get(1).expect("regex capture group 1").as_str();
let pattern_clause = captures.get(2).expect("regex capture group 2").as_str();
let action_clause = captures.get(3).expect("regex capture group 3").as_str();
match self.parse_clips_rule(rule_name, pattern_clause, action_clause) {
Ok(rule) => {
if !rule.body.is_empty() || !rule.head.is_empty() {
rules.push(rule);
warnings.push(MigrationWarning {
message: format!(
"CLIPS rule '{}' migrated - procedural attachments and advanced features not supported",
rule_name
),
line: Some(line_num + 1),
severity: WarningSeverity::Info,
});
} else {
warnings.push(MigrationWarning {
message: format!(
"CLIPS rule '{}' skipped - no parseable patterns found",
rule_name
),
line: Some(line_num + 1),
severity: WarningSeverity::Warning,
});
}
}
Err(e) => {
warnings.push(MigrationWarning {
message: format!("Failed to parse CLIPS rule '{}': {}", rule_name, e),
line: Some(line_num + 1),
severity: WarningSeverity::Error,
});
}
}
}
info!("Migrated {} CLIPS rules", rules.len());
Ok(rules)
}
fn parse_clips_rule(
&self,
name: &str,
pattern_clause: &str,
action_clause: &str,
) -> Result<Rule> {
let body = self.parse_clips_patterns(pattern_clause)?;
let head = self.parse_clips_actions(action_clause)?;
if body.is_empty() && head.is_empty() {
warn!("CLIPS rule '{}' has no parseable patterns or actions", name);
}
Ok(Rule {
name: name.to_string(),
body,
head,
})
}
fn parse_clips_patterns(&self, patterns: &str) -> Result<Vec<RuleAtom>> {
let mut atoms = Vec::new();
let pattern_regex = Regex::new(r"\((\w+)(?:\s+\(([^)]+)\))*\)")
.context("Failed to compile CLIPS pattern regex")?;
for captures in pattern_regex.captures_iter(patterns) {
let template_name = captures.get(1).expect("regex capture group 1").as_str();
if template_name == "test"
|| template_name == "not"
|| template_name == "and"
|| template_name == "or"
{
continue;
}
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("{}Instance", template_name)),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant(template_name.to_string()),
});
if let Some(slots) = captures.get(2) {
let slot_text = slots.as_str();
let slot_parts: Vec<&str> = slot_text.split_whitespace().collect();
if slot_parts.len() >= 2 {
let slot_name = slot_parts[0];
let slot_value = slot_parts[1];
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("{}Instance", template_name)),
predicate: Term::Constant(slot_name.to_string()),
object: self.parse_clips_value(slot_value),
});
}
}
}
Ok(atoms)
}
fn parse_clips_actions(&self, actions: &str) -> Result<Vec<RuleAtom>> {
let mut atoms = Vec::new();
let assert_regex = Regex::new(r"\(assert\s+\((\w+)(?:\s+\(([^)]+)\))*\)\)")
.context("Failed to compile CLIPS assert regex")?;
for captures in assert_regex.captures_iter(actions) {
let template_name = captures.get(1).expect("regex capture group 1").as_str();
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("new{}", template_name)),
predicate: Term::Constant("rdf:type".to_string()),
object: Term::Constant(template_name.to_string()),
});
if let Some(slots) = captures.get(2) {
let slot_text = slots.as_str();
let slot_parts: Vec<&str> = slot_text.split_whitespace().collect();
if slot_parts.len() >= 2 {
let slot_name = slot_parts[0];
let slot_value = slot_parts[1];
atoms.push(RuleAtom::Triple {
subject: Term::Variable(format!("new{}", template_name)),
predicate: Term::Constant(slot_name.to_string()),
object: self.parse_clips_value(slot_value),
});
}
}
}
let modify_regex = Regex::new(r"\(modify\s+\?(\w+)\s+\((\w+)\s+([^)]+)\)\)")
.context("Failed to compile CLIPS modify regex")?;
for captures in modify_regex.captures_iter(actions) {
let var_name = captures.get(1).expect("regex capture group 1").as_str();
let slot_name = captures.get(2).expect("regex capture group 2").as_str();
let slot_value = captures.get(3).expect("regex capture group 3").as_str();
atoms.push(RuleAtom::Triple {
subject: Term::Variable(var_name.to_string()),
predicate: Term::Constant(slot_name.to_string()),
object: self.parse_clips_value(slot_value),
});
}
Ok(atoms)
}
fn parse_clips_value(&self, value: &str) -> Term {
let value = value.trim();
if let Some(var_name) = value.strip_prefix('?') {
return Term::Variable(var_name.to_string());
}
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
return Term::Literal(value[1..value.len() - 1].to_string());
}
if value
.chars()
.all(|c| c.is_numeric() || c == '.' || c == '-')
{
Term::Literal(value.to_string())
} else {
Term::Constant(value.to_string())
}
}
fn expand_namespace(&self, term: &str) -> String {
if let Some(colon_pos) = term.find(':') {
let prefix = &term[..colon_pos];
let local = &term[colon_pos + 1..];
if let Some(namespace) = self.namespace_mappings.get(prefix) {
return format!("{}{}", namespace, local);
}
}
term.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_migration_tool_creation() {
let tool = MigrationTool::new();
assert!(!tool.strict_mode);
assert!(tool.namespace_mappings.is_empty());
}
#[test]
fn test_migration_tool_with_strict_mode() {
let tool = MigrationTool::new().with_strict_mode(true);
assert!(tool.strict_mode);
}
#[test]
fn test_namespace_mapping() {
let mut tool = MigrationTool::new();
tool.add_namespace_mapping("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
assert_eq!(
tool.namespace_mappings.get("rdf"),
Some(&"http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string())
);
}
#[test]
fn test_jena_simple_rule_migration() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let jena_rules = r#"
[rule1: (?a rdf:type Person) -> (?a rdf:type Human)]
"#;
let result = tool.migrate(jena_rules, SourceFormat::Jena)?;
assert_eq!(result.rules.len(), 1);
assert_eq!(result.rules[0].name, "rule1");
assert_eq!(result.source_format, SourceFormat::Jena);
Ok(())
}
#[test]
fn test_jena_multiple_rules() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let jena_rules = r#"
[rule1: (?x parent ?y) -> (?y hasParent ?x)]
[rule2: (?a knows ?b) -> (?b knows ?a)]
"#;
let result = tool.migrate(jena_rules, SourceFormat::Jena)?;
assert_eq!(result.rules.len(), 2);
assert_eq!(result.rules[0].name, "rule1");
assert_eq!(result.rules[1].name, "rule2");
Ok(())
}
#[test]
fn test_jena_with_comments() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let jena_rules = r#"
# This is a comment
[rule1: (?x parent ?y) -> (?y hasParent ?x)]
# Another comment
[rule2: (?a knows ?b) -> (?b knows ?a)]
"#;
let result = tool.migrate(jena_rules, SourceFormat::Jena)?;
assert_eq!(result.rules.len(), 2);
Ok(())
}
#[test]
fn test_parse_jena_term_variable() -> Result<(), Box<dyn std::error::Error>> {
let tool = MigrationTool::new();
let term = tool.parse_jena_term("?x")?;
assert!(matches!(term, Term::Variable(v) if v == "x"));
Ok(())
}
#[test]
fn test_parse_jena_term_constant() -> Result<(), Box<dyn std::error::Error>> {
let tool = MigrationTool::new();
let term = tool.parse_jena_term("rdf:type")?;
assert!(matches!(term, Term::Constant(c) if c == "rdf:type"));
Ok(())
}
#[test]
fn test_parse_jena_term_literal() -> Result<(), Box<dyn std::error::Error>> {
let tool = MigrationTool::new();
let term = tool.parse_jena_term("\"John\"")?;
assert!(matches!(term, Term::Literal(l) if l == "John"));
Ok(())
}
#[test]
fn test_migration_report_generation() {
let result = MigrationResult {
rules: vec![],
warnings: vec![MigrationWarning {
message: "Test warning".to_string(),
line: Some(1),
severity: WarningSeverity::Warning,
}],
source_format: SourceFormat::Jena,
original_count: 10,
migrated_count: 9,
};
let report = result.generate_report();
assert!(report.contains("Apache Jena"));
assert!(report.contains("Original rules: 10"));
assert!(report.contains("Migrated rules: 9"));
assert!(report.contains("Test warning"));
}
#[test]
fn test_migration_success_check() {
let result_success = MigrationResult {
rules: vec![],
warnings: vec![MigrationWarning {
message: "Info".to_string(),
line: None,
severity: WarningSeverity::Info,
}],
source_format: SourceFormat::Jena,
original_count: 1,
migrated_count: 1,
};
assert!(result_success.is_successful());
let result_failure = MigrationResult {
rules: vec![],
warnings: vec![MigrationWarning {
message: "Error".to_string(),
line: None,
severity: WarningSeverity::Error,
}],
source_format: SourceFormat::Jena,
original_count: 1,
migrated_count: 0,
};
assert!(!result_failure.is_successful());
}
#[test]
fn test_expand_namespace() {
let mut tool = MigrationTool::new();
tool.add_namespace_mapping("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
let expanded = tool.expand_namespace("rdf:type");
assert_eq!(expanded, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
}
#[test]
fn test_source_format_names() {
assert_eq!(SourceFormat::Jena.name(), "Apache Jena");
assert_eq!(SourceFormat::Drools.name(), "Drools DRL");
assert_eq!(SourceFormat::Clips.name(), "CLIPS");
}
#[test]
fn test_drools_simple_rule() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let drools_rules = r#"
rule "adult-rule"
when
Person(age == 25)
then
insert(new Adult(status: "verified"))
end
"#;
let result = tool.migrate(drools_rules, SourceFormat::Drools)?;
assert_eq!(result.rules.len(), 1);
assert_eq!(result.rules[0].name, "adult-rule");
assert!(!result.rules[0].body.is_empty());
assert!(!result.rules[0].head.is_empty());
Ok(())
}
#[test]
fn test_drools_with_insert() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let drools_rules = r#"
rule "create-employee"
when
Person(name == "John")
then
insert(new Employee(name: "John", role: "Developer"))
end
"#;
let result = tool.migrate(drools_rules, SourceFormat::Drools)?;
assert_eq!(result.rules.len(), 1);
let rule = &result.rules[0];
assert!(rule.head.iter().any(|atom| {
matches!(atom, RuleAtom::Triple {
predicate: Term::Constant(p),
object: Term::Constant(o), ..
} if p == "rdf:type" && o == "Employee")
}));
Ok(())
}
#[test]
fn test_drools_with_modify() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let drools_rules = r#"
rule "update-status"
when
Person(name == "Alice")
then
modify(person) { setStatus("active") }
end
"#;
let result = tool.migrate(drools_rules, SourceFormat::Drools)?;
assert_eq!(result.rules.len(), 1);
let rule = &result.rules[0];
assert!(rule.head.iter().any(|atom| {
matches!(atom, RuleAtom::Triple {
predicate: Term::Constant(p), ..
} if p == "Status")
}));
Ok(())
}
#[test]
fn test_drools_value_parsing() {
let tool = MigrationTool::new();
let value1 = tool.parse_drools_value("\"hello\"");
assert!(matches!(value1, Term::Literal(v) if v == "hello"));
let value2 = tool.parse_drools_value("$var");
assert!(matches!(value2, Term::Variable(v) if v == "var"));
let value3 = tool.parse_drools_value("42");
assert!(matches!(value3, Term::Literal(v) if v == "42"));
let value4 = tool.parse_drools_value("active");
assert!(matches!(value4, Term::Constant(v) if v == "active"));
}
#[test]
fn test_drools_condition_parsing() -> Result<(), Box<dyn std::error::Error>> {
let tool = MigrationTool::new();
let (field, value) = tool
.parse_drools_condition("age == 18")
.ok_or("expected Some value")?;
assert_eq!(field, "age");
assert_eq!(value, "18");
let (field2, value2) = tool
.parse_drools_condition("status : active")
.ok_or("expected Some value")?;
assert_eq!(field2, "status");
assert_eq!(value2, "active");
Ok(())
}
#[test]
fn test_clips_simple_rule() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let clips_rules = r#"
(defrule adult-check (person (age 25)) => (assert (adult (verified yes))))
"#;
let result = tool.migrate(clips_rules, SourceFormat::Clips)?;
assert_eq!(result.rules.len(), 1);
assert_eq!(result.rules[0].name, "adult-check");
Ok(())
}
#[test]
fn test_clips_with_assert() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let clips_rules = r#"
(defrule make-employee (person (name John)) => (assert (employee (name John))))
"#;
let result = tool.migrate(clips_rules, SourceFormat::Clips)?;
assert_eq!(result.rules.len(), 1);
assert_eq!(result.rules[0].name, "make-employee");
assert!(result.source_format == SourceFormat::Clips);
Ok(())
}
#[test]
fn test_clips_with_modify() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let clips_rules = r#"
(defrule update-status (person) => (modify ?person (status active)))
"#;
let result = tool.migrate(clips_rules, SourceFormat::Clips)?;
assert_eq!(result.rules.len(), 1);
assert_eq!(result.rules[0].name, "update-status");
assert!(result.source_format == SourceFormat::Clips);
Ok(())
}
#[test]
fn test_clips_value_parsing() -> Result<(), Box<dyn std::error::Error>> {
let tool = MigrationTool::new();
let value1 = tool.parse_clips_value("?x");
assert!(matches!(value1, Term::Variable(v) if v == "x"));
let value2 = tool.parse_clips_value("\"hello\"");
assert!(matches!(value2, Term::Literal(v) if v == "hello"));
let value3 = tool.parse_clips_value("42");
assert!(matches!(value3, Term::Literal(v) if v == "42"));
let value4 = tool.parse_clips_value("active");
assert!(matches!(value4, Term::Constant(v) if v == "active"));
Ok(())
}
#[test]
fn test_drools_multiple_conditions() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let drools_rules = r#"
rule "complex-rule"
when
Person(age == 25, status == "active")
then
insert(new Adult(verified: true))
end
"#;
let result = tool.migrate(drools_rules, SourceFormat::Drools)?;
assert_eq!(result.rules.len(), 1);
let rule = &result.rules[0];
assert!(rule.body.len() >= 2);
Ok(())
}
#[test]
fn test_migration_warnings_severity() {
let result = MigrationResult {
rules: vec![],
warnings: vec![
MigrationWarning {
message: "Info".to_string(),
line: Some(1),
severity: WarningSeverity::Info,
},
MigrationWarning {
message: "Warning".to_string(),
line: Some(2),
severity: WarningSeverity::Warning,
},
MigrationWarning {
message: "Error".to_string(),
line: Some(3),
severity: WarningSeverity::Error,
},
],
source_format: SourceFormat::Jena,
original_count: 3,
migrated_count: 0,
};
let report = result.generate_report();
assert!(report.contains("[INFO]"));
assert!(report.contains("[WARN]"));
assert!(report.contains("[ERROR]"));
}
#[test]
fn test_empty_drools_rule() -> Result<(), Box<dyn std::error::Error>> {
let mut tool = MigrationTool::new();
let drools_rules = r#"
rule "empty"
when
then
end
"#;
let result = tool.migrate(drools_rules, SourceFormat::Drools)?;
assert!(result.source_format == SourceFormat::Drools);
if !result.rules.is_empty() {
assert!(result.rules[0].body.is_empty() && result.rules[0].head.is_empty());
}
Ok(())
}
#[test]
fn test_clips_pattern_filtering() -> Result<(), Box<dyn std::error::Error>> {
let tool = MigrationTool::new();
let patterns = "(test (> ?x 5)) (person (age ?x))";
let atoms = tool.parse_clips_patterns(patterns)?;
assert!(atoms.iter().any(|atom| {
matches!(atom, RuleAtom::Triple {
object: Term::Constant(o), ..
} if o == "person")
}));
Ok(())
}
}