use super::content_hash::{compute_asset_id, AssetIdError};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum GeneCategory {
Repair,
Optimize,
Innovate,
}
impl Default for GeneCategory {
fn default() -> Self {
Self::Repair
}
}
impl std::fmt::Display for GeneCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GeneCategory::Repair => write!(f, "repair"),
GeneCategory::Optimize => write!(f, "optimize"),
GeneCategory::Innovate => write!(f, "innovate"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SignalPattern {
Substring(String),
Regex(String),
Alias(String),
}
impl SignalPattern {
pub fn matches(&self, signal: &str) -> bool {
match self {
SignalPattern::Substring(s) => signal.to_lowercase().contains(&s.to_lowercase()),
SignalPattern::Regex(pattern) => {
if let Ok(re) = regex_lite::Regex::new(pattern) {
re.is_match(signal)
} else {
false
}
}
SignalPattern::Alias(aliases) => aliases.split('|').any(|lang| {
let lang = lang.trim().to_lowercase();
signal.to_lowercase().contains(&lang)
}),
}
}
}
impl From<String> for SignalPattern {
fn from(s: String) -> Self {
if s.starts_with('/') && s.ends_with('/') {
SignalPattern::Regex(s.trim_matches('/').to_string())
} else if s.contains('|') {
SignalPattern::Alias(s)
} else {
SignalPattern::Substring(s)
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct GeneConstraints {
#[serde(rename = "max_files")]
pub max_files: usize,
#[serde(rename = "forbidden_paths")]
pub forbidden_paths: Vec<String>,
}
impl GeneConstraints {
pub fn new(max_files: usize) -> Self {
Self {
max_files,
forbidden_paths: vec![],
}
}
pub fn with_forbidden(mut self, paths: Vec<String>) -> Self {
self.forbidden_paths = paths;
self
}
pub fn is_allowed(&self, path: &str) -> bool {
!self
.forbidden_paths
.iter()
.any(|forbidden| path.contains(forbidden))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GenePrecondition {
pub description: String,
#[serde(default)]
pub check: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EpigeneticMark {
pub name: String,
pub value: serde_json::Value,
#[serde(default = "default_true")]
pub active: bool,
}
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GepGene {
#[serde(rename = "type")]
pub gene_type: String,
#[serde(rename = "schema_version")]
pub schema_version: String,
pub id: String,
pub category: GeneCategory,
#[serde(rename = "signals_match")]
pub signals_match: Vec<SignalPattern>,
#[serde(default)]
pub preconditions: Vec<GenePrecondition>,
pub strategy: Vec<String>,
pub constraints: GeneConstraints,
pub validation: Vec<String>,
#[serde(default, rename = "epigenetic_marks")]
pub epigenetic_marks: Vec<EpigeneticMark>,
#[serde(default)]
pub model_name: Option<String>,
#[serde(rename = "asset_id")]
pub asset_id: String,
}
impl GepGene {
pub fn new(
id: String,
category: GeneCategory,
signals_match: Vec<String>,
strategy: Vec<String>,
validation: Vec<String>,
) -> Result<Self, AssetIdError> {
let signals_match: Vec<SignalPattern> =
signals_match.into_iter().map(SignalPattern::from).collect();
let constraints = GeneConstraints::new(20);
let mut gene = Self {
gene_type: "Gene".to_string(),
schema_version: super::GEP_SCHEMA_VERSION.to_string(),
id,
category,
signals_match,
preconditions: vec![],
strategy,
constraints,
validation,
epigenetic_marks: vec![],
model_name: None,
asset_id: String::new(), };
gene.asset_id = compute_asset_id(&gene, &["asset_id"])?;
Ok(gene)
}
pub fn matches_signals(&self, signals: &[String]) -> usize {
let mut score = 0;
for signal in signals {
for pattern in &self.signals_match {
if pattern.matches(signal) {
score += 1;
break;
}
}
}
score
}
pub fn validate(&self) -> Result<(), String> {
if self.id.is_empty() {
return Err("Gene id cannot be empty".to_string());
}
if self.strategy.is_empty() {
return Err("Gene strategy cannot be empty".to_string());
}
if self.validation.is_empty() {
return Err("Gene validation cannot be empty".to_string());
}
Ok(())
}
}
impl From<&crate::Gene> for GepGene {
fn from(oris_gene: &crate::Gene) -> Self {
let signals_match: Vec<SignalPattern> = oris_gene
.signals
.iter()
.map(|s| SignalPattern::from(s.clone()))
.collect();
let constraints = GeneConstraints::new(20);
GepGene {
gene_type: "Gene".to_string(),
schema_version: super::GEP_SCHEMA_VERSION.to_string(),
id: oris_gene.id.clone(),
category: GeneCategory::Repair, signals_match,
preconditions: vec![],
strategy: oris_gene.strategy.clone(),
constraints,
validation: oris_gene.validation.clone(),
epigenetic_marks: vec![],
model_name: None,
asset_id: oris_gene.id.clone(), }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_pattern_substring() {
let pattern = SignalPattern::from("timeout".to_string());
assert!(pattern.matches("connection timeout error"));
assert!(pattern.matches("TIMEOUT DETECTED"));
assert!(!pattern.matches("time out"));
}
#[test]
fn test_signal_pattern_regex() {
let pattern = SignalPattern::from("error".to_string());
assert!(pattern.matches("error: need to retry"));
assert!(pattern.matches("ERROR RETRY"));
assert!(!pattern.matches("success"));
}
#[test]
fn test_signal_pattern_alias() {
let pattern = SignalPattern::from("en|zh|ja".to_string());
assert!(pattern.matches("en: hello"));
assert!(pattern.matches("zh: 你好"));
assert!(!pattern.matches("fr: bonjour"));
}
#[test]
fn test_gene_creation() {
let gene = GepGene::new(
"gene_test_001".to_string(),
GeneCategory::Repair,
vec!["timeout".to_string(), "error".to_string()],
vec!["Analyze error".to_string(), "Fix issue".to_string()],
vec!["cargo test".to_string()],
)
.unwrap();
assert_eq!(gene.gene_type, "Gene");
assert_eq!(gene.schema_version, "1.5.0");
assert!(gene.asset_id.starts_with("sha256:")); }
#[test]
fn test_gene_matches_signals() {
let gene = GepGene::new(
"gene_test_002".to_string(),
GeneCategory::Repair,
vec!["timeout".to_string(), "error".to_string()],
vec!["Fix".to_string()],
vec!["test".to_string()],
)
.unwrap();
let signals = vec![
"error: connection timeout".to_string(),
"perf_bottleneck".to_string(),
];
assert_eq!(gene.matches_signals(&signals), 1);
}
#[test]
fn test_gene_validate() {
let mut gene = GepGene::new(
"gene_test_003".to_string(),
GeneCategory::Repair,
vec!["timeout".to_string()],
vec![],
vec!["test".to_string()],
)
.unwrap();
assert!(gene.validate().is_err());
gene.strategy.push("do something".to_string());
assert!(gene.validate().is_ok());
}
}