1use axioval_engine::{
4 CapabilityEvaluation, CompiledRule, NotEvaluatedReason, ParameterDescriptor, ParameterType,
5 PropertyRequest, PropertyResolution, PropertyResolutionServiceHandle, RuleCapability,
6 RuleContext,
7};
8use axioval_ir::contract::ParameterValue;
9use axioval_ir::{Evidence, Finding, Object, PropertyValue, Severity};
10
11use crate::selection::{property_error, select_objects};
12
13fn property_reference<'a>(
14 rule: &'a CompiledRule,
15 name: &str,
16) -> Option<(Option<&'a str>, &'a str)> {
17 match rule.parameters.get(name)? {
18 ParameterValue::PropertyReference {
19 property,
20 property_set,
21 } => Some((property_set.as_deref(), property)),
22 _ => None,
23 }
24}
25
26fn string<'a>(rule: &'a CompiledRule, name: &str) -> Option<&'a str> {
27 match rule.parameters.get(name)? {
28 ParameterValue::String { value }
29 | ParameterValue::Enum { value }
30 | ParameterValue::Reference { value } => Some(value),
31 _ => None,
32 }
33}
34
35fn integer(rule: &CompiledRule, name: &str) -> Option<i64> {
36 match rule.parameters.get(name)? {
37 ParameterValue::Integer { value } => Some(*value),
38 _ => None,
39 }
40}
41
42fn finding(
43 rule: &CompiledRule,
44 object: &Object,
45 message: String,
46 evidence: Vec<Evidence>,
47) -> Finding {
48 Finding {
49 rule_id: rule.id.clone(),
50 object_id: object.id.clone(),
51 severity: match rule.severity {
52 axioval_ir::contract::Severity::Error => Severity::Error,
53 axioval_ir::contract::Severity::Warning => Severity::Warning,
54 axioval_ir::contract::Severity::Info => Severity::Info,
55 },
56 message,
57 evidence,
58 }
59}
60
61fn unavailable_selected(
62 selected: &[&Object],
63 reason: &NotEvaluatedReason,
64 message: &str,
65 mut evaluation: CapabilityEvaluation,
66) -> CapabilityEvaluation {
67 for object in selected {
68 evaluation.push_object_not_evaluated(object.id.clone(), reason.clone(), message);
69 }
70 evaluation
71}
72
73fn resolve_error(
74 evaluation: &mut CapabilityEvaluation,
75 object: &Object,
76 error: axioval_engine::PropertyResolutionError,
77) {
78 let (reason, message) = property_error(error);
79 evaluation.push_object_not_evaluated(object.id.clone(), reason, message);
80}
81
82pub struct PropertyExists;
84impl RuleCapability for PropertyExists {
85 fn id(&self) -> &'static str {
86 "axioval:capability.property-exists"
87 }
88
89 fn parameters(&self) -> Vec<ParameterDescriptor> {
90 vec![ParameterDescriptor::required(
91 "property",
92 ParameterType::PropertyReference,
93 )]
94 }
95
96 fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation {
97 let Some((set, name)) = property_reference(rule, "property") else {
98 return CapabilityEvaluation::not_evaluated(
99 NotEvaluatedReason::InvalidDeclaration,
100 "property-exists has no valid property reference",
101 );
102 };
103 let (selected, mut evaluation) = select_objects(context, &rule.selector);
104 let Some(service) = context.services.get::<PropertyResolutionServiceHandle>() else {
105 return unavailable_selected(
106 &selected,
107 &NotEvaluatedReason::MissingService,
108 "property-resolution service is not registered",
109 evaluation,
110 );
111 };
112 for object in selected {
113 let request =
114 match PropertyRequest::try_new(object.id.clone(), set.map(ToOwned::to_owned), name)
115 {
116 Ok(request) => request,
117 Err(error) => {
118 evaluation.push_not_evaluated(
119 NotEvaluatedReason::InvalidDeclaration,
120 error.to_string(),
121 );
122 return evaluation;
123 }
124 };
125 match service.resolve(&request) {
126 Ok(PropertyResolution::Present(_)) => {}
127 Ok(PropertyResolution::Absent(proof)) => evaluation.push_finding(finding(
128 rule,
129 object,
130 format!("missing exact property {name}"),
131 vec![proof.evidence().clone()],
132 )),
133 Err(error) => resolve_error(&mut evaluation, object, error),
134 }
135 }
136 evaluation
137 }
138}
139
140pub struct PropertyRequired;
145impl RuleCapability for PropertyRequired {
146 fn id(&self) -> &'static str {
147 "axioval:capability.property-required"
148 }
149
150 fn parameters(&self) -> Vec<ParameterDescriptor> {
151 vec![ParameterDescriptor::required(
152 "property",
153 ParameterType::PropertyReference,
154 )]
155 }
156
157 fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation {
158 let Some((set, name)) = property_reference(rule, "property") else {
159 return CapabilityEvaluation::not_evaluated(
160 NotEvaluatedReason::InvalidDeclaration,
161 "property-required has no valid property reference",
162 );
163 };
164 let (selected, mut evaluation) = select_objects(context, &rule.selector);
165 let Some(service) = context.services.get::<PropertyResolutionServiceHandle>() else {
166 return unavailable_selected(
167 &selected,
168 &NotEvaluatedReason::MissingService,
169 "property-resolution service is not registered",
170 evaluation,
171 );
172 };
173 for object in selected {
174 let request =
175 match PropertyRequest::try_new(object.id.clone(), set.map(ToOwned::to_owned), name)
176 {
177 Ok(request) => request,
178 Err(error) => {
179 evaluation.push_not_evaluated(
180 NotEvaluatedReason::InvalidDeclaration,
181 error.to_string(),
182 );
183 return evaluation;
184 }
185 };
186 match service.resolve(&request) {
187 Ok(PropertyResolution::Present(resolved)) => {
188 let value = &resolved.property().value;
189 if matches!(value, PropertyValue::Null)
190 || matches!(value, PropertyValue::String(text) if text.trim().is_empty())
191 {
192 evaluation.push_finding(finding(
193 rule,
194 object,
195 format!("missing required property {name}"),
196 resolved.property().evidence.clone().into_iter().collect(),
197 ));
198 }
199 }
200 Ok(PropertyResolution::Absent(proof)) => evaluation.push_finding(finding(
201 rule,
202 object,
203 format!("missing required property {name}"),
204 vec![proof.evidence().clone()],
205 )),
206 Err(error) => resolve_error(&mut evaluation, object, error),
207 }
208 }
209 evaluation
210 }
211}
212
213fn boolean(rule: &CompiledRule, name: &str) -> Option<bool> {
214 match rule.parameters.get(name)? {
215 ParameterValue::Boolean { value } => Some(*value),
216 _ => None,
217 }
218}
219
220pub struct BooleanPropertyEquals;
222impl RuleCapability for BooleanPropertyEquals {
223 fn id(&self) -> &'static str {
224 "axioval:capability.property-value-equals"
225 }
226
227 fn parameters(&self) -> Vec<ParameterDescriptor> {
228 vec![
229 ParameterDescriptor::required("property", ParameterType::PropertyReference),
230 ParameterDescriptor::required("expected", ParameterType::Boolean),
231 ]
232 }
233
234 fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation {
235 let (Some((set, name)), Some(expected)) = (
236 property_reference(rule, "property"),
237 boolean(rule, "expected"),
238 ) else {
239 return CapabilityEvaluation::not_evaluated(
240 NotEvaluatedReason::InvalidDeclaration,
241 "property-value-equals parameters are invalid",
242 );
243 };
244 let (selected, mut evaluation) = select_objects(context, &rule.selector);
245 let Some(service) = context.services.get::<PropertyResolutionServiceHandle>() else {
246 return unavailable_selected(
247 &selected,
248 &NotEvaluatedReason::MissingService,
249 "property-resolution service is not registered",
250 evaluation,
251 );
252 };
253 for object in selected {
254 let request =
255 match PropertyRequest::try_new(object.id.clone(), set.map(ToOwned::to_owned), name)
256 {
257 Ok(request) => request,
258 Err(error) => {
259 evaluation.push_not_evaluated(
260 NotEvaluatedReason::InvalidDeclaration,
261 error.to_string(),
262 );
263 return evaluation;
264 }
265 };
266 match service.resolve(&request) {
267 Ok(PropertyResolution::Present(resolved)) => {
268 let property = resolved.property();
269 match &property.value {
270 PropertyValue::Boolean(actual) if *actual == expected => {}
271 PropertyValue::Boolean(_) => evaluation.push_finding(finding(
272 rule,
273 object,
274 format!("property {name} does not equal {expected}"),
275 property.evidence.clone().into_iter().collect(),
276 )),
277 _ => evaluation.push_object_not_evaluated(
278 object.id.clone(),
279 NotEvaluatedReason::InvalidEvidence,
280 format!("property {name} is not boolean"),
281 ),
282 }
283 }
284 Ok(PropertyResolution::Absent(proof)) => evaluation.push_finding(finding(
285 rule,
286 object,
287 format!("property {name} does not equal {expected}"),
288 vec![proof.evidence().clone()],
289 )),
290 Err(error) => resolve_error(&mut evaluation, object, error),
291 }
292 }
293 evaluation
294 }
295}
296
297#[derive(Clone, Copy)]
298enum IntegerOperator {
299 Equal,
300 NotEqual,
301 GreaterThan,
302 GreaterOrEqual,
303 LessThan,
304 LessOrEqual,
305}
306impl IntegerOperator {
307 fn parse(value: &str) -> Option<Self> {
308 match value {
309 "equal" => Some(Self::Equal),
310 "not_equal" => Some(Self::NotEqual),
311 "greater_than" => Some(Self::GreaterThan),
312 "greater_or_equal" => Some(Self::GreaterOrEqual),
313 "less_than" => Some(Self::LessThan),
314 "less_or_equal" => Some(Self::LessOrEqual),
315 _ => None,
316 }
317 }
318 fn passes(self, actual: i64, expected: i64) -> bool {
319 match self {
320 Self::Equal => actual == expected,
321 Self::NotEqual => actual != expected,
322 Self::GreaterThan => actual > expected,
323 Self::GreaterOrEqual => actual >= expected,
324 Self::LessThan => actual < expected,
325 Self::LessOrEqual => actual <= expected,
326 }
327 }
328}
329
330pub struct PropertyPredicate;
332impl RuleCapability for PropertyPredicate {
333 fn id(&self) -> &'static str {
334 "axioval:capability.property-predicate"
335 }
336
337 fn parameters(&self) -> Vec<ParameterDescriptor> {
338 vec![
339 ParameterDescriptor::required("property_set", ParameterType::String),
340 ParameterDescriptor::required("property", ParameterType::String),
341 ParameterDescriptor::required("operator", ParameterType::String),
342 ParameterDescriptor::required("value", ParameterType::Integer),
343 ]
344 }
345
346 fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation {
347 let (Some(set), Some(name), Some(operator), Some(expected)) = (
348 string(rule, "property_set"),
349 string(rule, "property"),
350 string(rule, "operator"),
351 integer(rule, "value"),
352 ) else {
353 return CapabilityEvaluation::not_evaluated(
354 NotEvaluatedReason::InvalidDeclaration,
355 "property-predicate parameters are invalid",
356 );
357 };
358 let Some(operator) = IntegerOperator::parse(operator) else {
359 return CapabilityEvaluation::not_evaluated(
360 NotEvaluatedReason::InvalidDeclaration,
361 "property-predicate operator is unsupported",
362 );
363 };
364 let (selected, mut evaluation) = select_objects(context, &rule.selector);
365 let Some(service) = context.services.get::<PropertyResolutionServiceHandle>() else {
366 return unavailable_selected(
367 &selected,
368 &NotEvaluatedReason::MissingService,
369 "property-resolution service is not registered",
370 evaluation,
371 );
372 };
373 for object in selected {
374 let request =
375 match PropertyRequest::try_new(object.id.clone(), Some(set.to_owned()), name) {
376 Ok(request) => request,
377 Err(error) => {
378 evaluation.push_not_evaluated(
379 NotEvaluatedReason::InvalidDeclaration,
380 error.to_string(),
381 );
382 return evaluation;
383 }
384 };
385 match service.resolve(&request) {
386 Ok(PropertyResolution::Present(resolved)) => {
387 let property = resolved.property();
388 let actual = match &property.value {
389 PropertyValue::Integer(value) => Some(*value),
390 _ => None,
391 };
392 if !actual.is_some_and(|actual| operator.passes(actual, expected)) {
393 evaluation.push_finding(finding(
394 rule,
395 object,
396 format!(
397 "property {set}.{name} does not satisfy {} {expected}",
398 string(rule, "operator").unwrap_or("invalid")
399 ),
400 property.evidence.clone().into_iter().collect(),
401 ));
402 }
403 }
404 Ok(PropertyResolution::Absent(proof)) => evaluation.push_finding(finding(
405 rule,
406 object,
407 format!(
408 "property {set}.{name} does not satisfy {} {expected}",
409 string(rule, "operator").unwrap_or("invalid")
410 ),
411 vec![proof.evidence().clone()],
412 )),
413 Err(error) => resolve_error(&mut evaluation, object, error),
414 }
415 }
416 evaluation
417 }
418}