use serde::{Deserialize, Serialize};
use crate::core::error::{AnamError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackRule {
pub name: String,
pub datalog: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackModelRef {
pub name: String,
pub artifact_path: String,
pub num_features: usize,
pub avg_latency_ms: f64,
pub accuracy: f64,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogicPack {
pub name: String,
pub version: String,
pub description: Option<String>,
pub author: Option<String>,
pub rules: Vec<PackRule>,
pub models: Vec<PackModelRef>,
}
impl LogicPack {
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json)
.map_err(|e| AnamError::Logic(format!("failed to parse Logic Pack JSON: {e}")))
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self)
.map_err(|e| AnamError::Logic(format!("failed to serialize Logic Pack: {e}")))
}
pub fn from_file(path: &str) -> Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| {
AnamError::Logic(format!("failed to read Logic Pack file '{path}': {e}"))
})?;
Self::from_json(&content)
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn model_count(&self) -> usize {
self.models.len()
}
pub fn summary(&self) -> String {
let mut lines = vec![format!("Logic Pack: {} v{}", self.name, self.version)];
if let Some(desc) = &self.description {
lines.push(format!(" {desc}"));
}
if let Some(author) = &self.author {
lines.push(format!(" Author: {author}"));
}
lines.push(format!(
" {} rule(s), {} model(s)",
self.rules.len(),
self.models.len()
));
for rule in &self.rules {
lines.push(format!(" • {} ← {}", rule.name, rule.datalog));
}
for model in &self.models {
lines.push(format!(
" ◆ {} [{}] — {:.1}ms, {:.0}% accuracy",
model.name,
model.artifact_path,
model.avg_latency_ms,
model.accuracy * 100.0
));
}
lines.join("\n")
}
}
pub struct LogicPackBuilder {
name: String,
version: String,
description: Option<String>,
author: Option<String>,
rules: Vec<PackRule>,
models: Vec<PackModelRef>,
}
impl LogicPackBuilder {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
description: None,
author: None,
rules: Vec::new(),
models: Vec::new(),
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn rule(mut self, name: impl Into<String>, datalog: impl Into<String>) -> Self {
self.rules.push(PackRule {
name: name.into(),
datalog: datalog.into(),
description: None,
});
self
}
pub fn rule_with_desc(
mut self,
name: impl Into<String>,
datalog: impl Into<String>,
desc: impl Into<String>,
) -> Self {
self.rules.push(PackRule {
name: name.into(),
datalog: datalog.into(),
description: Some(desc.into()),
});
self
}
pub fn model_ref(
mut self,
name: impl Into<String>,
path: impl Into<String>,
num_features: usize,
avg_latency_ms: f64,
accuracy: f64,
) -> Self {
self.models.push(PackModelRef {
name: name.into(),
artifact_path: path.into(),
num_features,
avg_latency_ms,
accuracy,
description: None,
});
self
}
pub fn build(self) -> LogicPack {
LogicPack {
name: self.name,
version: self.version,
description: self.description,
author: self.author,
rules: self.rules,
models: self.models,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_logic_pack() {
let pack = LogicPackBuilder::new("financial_compliance", "1.0.0")
.description("AML/KYC transaction rules")
.author("Jorge Martinez")
.rule("high_risk", "fraud_prob > 0.90 AND amount > 10000")
.rule(
"wire_alert",
"merchant_type = 'wire_transfer' AND amount > 50000",
)
.model_ref(
"fraud_detector",
"demo/models/fraud_detector.onnx",
3,
5.0,
0.95,
)
.build();
assert_eq!(pack.name, "financial_compliance");
assert_eq!(pack.rule_count(), 2);
assert_eq!(pack.model_count(), 1);
}
#[test]
fn serde_roundtrip() {
let pack = LogicPackBuilder::new("test_pack", "0.1.0")
.rule("r1", "x > 10")
.build();
let json = pack.to_json().unwrap();
let restored = LogicPack::from_json(&json).unwrap();
assert_eq!(restored.name, "test_pack");
assert_eq!(restored.rules.len(), 1);
}
}