tempus_engine/
metadata.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::error::ValidationError;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct RuleDefinition {
13 pub name: String,
15
16 pub logic: Value,
18
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub version: Option<String>,
22
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub description: Option<String>,
26
27 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub tags: Vec<String>,
30
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub required_context_keys: Vec<String>,
35}
36
37impl RuleDefinition {
38 pub fn new(name: impl Into<String>, logic: Value) -> Self {
40 Self {
41 name: name.into(),
42 logic,
43 version: None,
44 description: None,
45 tags: Vec::new(),
46 required_context_keys: Vec::new(),
47 }
48 }
49
50 pub fn with_version(mut self, version: impl Into<String>) -> Self {
52 self.version = Some(version.into());
53 self
54 }
55
56 pub fn with_description(mut self, description: impl Into<String>) -> Self {
58 self.description = Some(description.into());
59 self
60 }
61
62 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
64 self.tags = tags;
65 self
66 }
67
68 pub fn with_required_keys(mut self, keys: Vec<String>) -> Self {
70 self.required_context_keys = keys;
71 self
72 }
73
74 pub fn validate_context(&self, context: &Value) -> Result<(), ValidationError> {
79 if self.required_context_keys.is_empty() {
80 return Ok(());
81 }
82 let obj = match context.as_object() {
83 Some(o) => o,
84 None => {
85 return Err(ValidationError::new(
86 &self.name,
87 self.required_context_keys.clone(),
88 ))
89 }
90 };
91 let missing: Vec<String> = self
92 .required_context_keys
93 .iter()
94 .filter(|k| !obj.contains_key(k.as_str()))
95 .cloned()
96 .collect();
97 if missing.is_empty() {
98 Ok(())
99 } else {
100 Err(ValidationError::new(&self.name, missing))
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use serde_json::json;
109
110 #[test]
111 fn new_creates_minimal_definition() {
112 let rule = RuleDefinition::new("test", json!({"==": [1, 1]}));
113 assert_eq!(rule.name, "test");
114 assert_eq!(rule.version, None);
115 assert_eq!(rule.description, None);
116 assert!(rule.tags.is_empty());
117 assert!(rule.required_context_keys.is_empty());
118 }
119
120 #[test]
121 fn builder_pattern_works() {
122 let rule = RuleDefinition::new("fraud-block", json!({">":[{"var":"risk"},0.8]}))
123 .with_version("2.1.0")
124 .with_description("Block high-risk transactions")
125 .with_tags(vec!["fraud".into(), "prod".into()]);
126
127 assert_eq!(rule.name, "fraud-block");
128 assert_eq!(rule.version.as_deref(), Some("2.1.0"));
129 assert_eq!(
130 rule.description.as_deref(),
131 Some("Block high-risk transactions")
132 );
133 assert_eq!(rule.tags, vec!["fraud", "prod"]);
134 }
135
136 #[test]
137 fn serialization_roundtrip() {
138 let rule = RuleDefinition::new("test", json!({">":[{"var":"x"},1]})).with_version("1.0.0");
139
140 let json_str = serde_json::to_string(&rule).unwrap();
141 let deserialized: RuleDefinition = serde_json::from_str(&json_str).unwrap();
142 assert_eq!(rule, deserialized);
143 }
144
145 #[test]
146 fn minimal_serialization_omits_empty_fields() {
147 let rule = RuleDefinition::new("test", json!({"==": [1, 1]}));
148 let json_str = serde_json::to_string(&rule).unwrap();
149 assert!(!json_str.contains("version"));
150 assert!(!json_str.contains("description"));
151 assert!(!json_str.contains("tags"));
152 }
153
154 #[test]
155 fn validate_context_passes_when_all_keys_present() {
156 let rule = RuleDefinition::new("r", json!({}))
157 .with_required_keys(vec!["score".into(), "income".into()]);
158 let ctx = json!({"score": 700, "income": 50000});
159 assert!(rule.validate_context(&ctx).is_ok());
160 }
161
162 #[test]
163 fn validate_context_fails_on_missing_key() {
164 let rule = RuleDefinition::new("r", json!({}))
165 .with_required_keys(vec!["score".into(), "income".into()]);
166 let ctx = json!({"score": 700});
167 let err = rule.validate_context(&ctx).unwrap_err();
168 assert!(err.missing_keys.contains(&"income".to_string()));
169 }
170
171 #[test]
172 fn validate_context_passes_with_no_required_keys() {
173 let rule = RuleDefinition::new("r", json!({}));
174 assert!(rule.validate_context(&json!({})).is_ok());
175 }
176}
177