use tower_rules::TowerRule;
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("YAML parse error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
}
pub fn parse_rule_yaml(src: &str) -> Result<TowerRule, ParseError> {
Ok(serde_yaml::from_str(src)?)
}
pub fn parse_rule_json(src: &str) -> Result<TowerRule, ParseError> {
Ok(serde_json::from_str(src)?)
}
#[cfg(test)]
mod tests {
use tower_rules::*;
use super::*;
#[test]
fn parse_yaml_scryer_health_rule() {
let yaml = r#"
schema_version: V1
id: yubaba-quorum-loss
name: "Yubaba raft quorum loss"
predicate:
type: scryer_health
signal: yubaba_raft_quorum_lost
trigger:
type: notification
channels:
- type: desktop_badge
severity: critical
debounce_ms: 30000
federation:
type: local_only
enabled: true
"#;
let rule = parse_rule_yaml(yaml).unwrap();
assert_eq!(rule.id, RuleId("yubaba-quorum-loss".into()));
assert!(matches!(
rule.predicate,
Predicate::ScryerHealth { signal: HealthSignal::YubabaRaftQuorumLost }
));
assert!(matches!(
rule.trigger,
Trigger::Notification { severity: Severity::Critical, .. }
));
assert_eq!(rule.debounce_ms, Some(30_000));
}
#[test]
fn parse_yaml_event_match_rule() {
let yaml = r#"
schema_version: V1
id: db-error-burst
name: "Database error burst"
predicate:
type: event_match
scope:
type: service
ident: "noisetable-db.pdx"
level: error
target:
type: glob
pattern: "database.*"
rate:
min_count: 5
window_ms: 60000
trigger:
type: notification
channels:
- type: desktop_badge
severity: critical
federation:
type: local_only
enabled: true
"#;
let rule = parse_rule_yaml(yaml).unwrap();
assert_eq!(rule.id, RuleId("db-error-burst".into()));
assert!(matches!(
&rule.predicate,
Predicate::EventMatch { level: Some(LevelFilter::Error), .. }
));
}
#[test]
fn parse_yaml_compound_and_rule() {
let yaml = r#"
schema_version: V1
id: compound-test
name: "Compound test"
predicate:
type: compound
op: and
children:
- type: scryer_health
signal: yubaba_raft_quorum_lost
- type: event_match
scope:
type: any
level: warn
trigger:
type: notification
channels:
- type: desktop_badge
severity: warning
federation:
type: local_only
enabled: false
"#;
let rule = parse_rule_yaml(yaml).unwrap();
assert!(matches!(rule.predicate, Predicate::Compound { op: CompoundOp::And, .. }));
assert!(!rule.enabled);
}
#[test]
fn parse_json_rule() {
let json = r#"{
"schema_version": "V1",
"id": "json-rule",
"name": "JSON rule",
"predicate": {"type": "scryer_health", "signal": "ingestion_lag"},
"trigger": {
"type": "notification",
"channels": [{"type": "desktop_badge"}],
"severity": "warning"
},
"federation": {"type": "local_only"},
"enabled": true
}"#;
let rule = parse_rule_json(json).unwrap();
assert_eq!(rule.id, RuleId("json-rule".into()));
assert!(matches!(
rule.predicate,
Predicate::ScryerHealth { signal: HealthSignal::IngestionLag }
));
}
#[test]
fn parse_yaml_invalid_syntax_errors() {
let bad = "predicate: {type: [}";
assert!(parse_rule_yaml(bad).is_err());
}
#[test]
fn parse_yaml_yubaba_action_trigger() {
let yaml = r#"
schema_version: V1
id: restart-on-crash
name: "Restart crashed workload"
predicate:
type: event_match
scope:
type: service
ident: "noisetable-api.pdx"
level: error
target:
type: exact
value: "service.crash"
trigger:
type: yubaba_action
kind:
type: restart_workload
target: "noisetable-api.pdx"
federation:
type: local_only
enabled: true
"#;
let rule = parse_rule_yaml(yaml).unwrap();
assert!(matches!(rule.trigger, Trigger::YubabaAction { .. }));
}
#[test]
fn parse_yaml_agent_dispatch_trigger() {
let yaml = r#"
schema_version: V1
id: db-repair-dispatch
name: "Dispatch DB repair agent"
predicate:
type: scryer_health
signal: yubaba_raft_quorum_lost
trigger:
type: agent_dispatch
agent_class: "gnome/db-repair"
prompt_template: "DB issue detected: {{event}}"
placement:
location:
kind: local
runtime: native
federation:
type: local_only
enabled: false
"#;
let rule = parse_rule_yaml(yaml).unwrap();
assert!(matches!(rule.trigger, Trigger::AgentDispatch { .. }));
}
}