|
1 | 1 | //! Audit lists and instances, as `parse_audit_instances` reads them with `null_as_empty`. |
2 | 2 |
|
| 3 | +use crate::header_metadata::_helpers::identity::check_identity; |
3 | 4 | use crate::header_metadata::_helpers::text::{ |
4 | | - entry, is_snake_case, non_blank_text, optional_bool, optional_count, optional_text, |
| 5 | + Site, entry, non_blank_text, optional_bool, optional_count, optional_text, |
5 | 6 | }; |
6 | 7 | use crate::header_metadata::constants::{AUDIT_OPTION_KEYS, AUDIT_SEVERITIES}; |
7 | | -use crate::header_metadata::models::{HeaderMetadataDeferral, ParsedAudit}; |
| 8 | +use crate::header_metadata::models::{HeaderMetadataStop, ParsedAudit}; |
8 | 9 | use crate::types::{AuthoredNode, NodeKind}; |
9 | 10 |
|
10 | 11 | /// Parse an authored audit list; Python's `None` is an empty list. |
11 | 12 | pub(crate) fn audit_list<N: AuthoredNode>( |
12 | 13 | node: &N, |
13 | | -) -> Result<Vec<ParsedAudit<N>>, HeaderMetadataDeferral> { |
| 14 | + site: Site<'_>, |
| 15 | +) -> Result<Vec<ParsedAudit<N>>, HeaderMetadataStop> { |
14 | 16 | match node.kind() { |
15 | 17 | NodeKind::Null => Ok(Vec::new()), |
16 | | - NodeKind::List => node.items().iter().map(audit_instance).collect(), |
17 | | - _ => Err(HeaderMetadataDeferral::Invalid), |
| 18 | + NodeKind::List => node |
| 19 | + .items() |
| 20 | + .iter() |
| 21 | + .map(|item| audit_instance(item, site)) |
| 22 | + .collect(), |
| 23 | + _ => Err(site.error("audits must be a list")), |
18 | 24 | } |
19 | 25 | } |
20 | 26 |
|
21 | | -fn audit_instance<N: AuthoredNode>(node: &N) -> Result<ParsedAudit<N>, HeaderMetadataDeferral> { |
22 | | - match node.kind() { |
23 | | - NodeKind::Str => { |
24 | | - identity(node)?; |
25 | | - Ok(bare_audit(node.clone())) |
26 | | - } |
27 | | - NodeKind::Map => { |
28 | | - let entries = node.entries(); |
29 | | - let [(definition_name, arguments)] = entries.as_slice() else { |
30 | | - return Err(HeaderMetadataDeferral::Invalid); |
31 | | - }; |
32 | | - identity(definition_name)?; |
33 | | - match arguments.kind() { |
34 | | - NodeKind::Null => Ok(bare_audit(definition_name.clone())), |
35 | | - NodeKind::Map => configured_audit(definition_name, &arguments.entries()), |
36 | | - _ => Err(HeaderMetadataDeferral::Invalid), |
37 | | - } |
38 | | - } |
39 | | - _ => Err(HeaderMetadataDeferral::Invalid), |
| 27 | +fn audit_instance<N: AuthoredNode>( |
| 28 | + node: &N, |
| 29 | + site: Site<'_>, |
| 30 | +) -> Result<ParsedAudit<N>, HeaderMetadataStop> { |
| 31 | + let definition_kind = format!("{} audit definition", site.label); |
| 32 | + if node.kind() == NodeKind::Str { |
| 33 | + let Some(text) = non_blank_text(node)? else { |
| 34 | + return Err(site.error("audits must not contain empty names")); |
| 35 | + }; |
| 36 | + check_identity(&text, &definition_kind, site.path)?; |
| 37 | + return Ok(bare_audit(node.clone())); |
| 38 | + } |
| 39 | + let entries = node.entries(); |
| 40 | + let [(definition_name, arguments)] = entries.as_slice() else { |
| 41 | + return Err(site.error("audits must be strings or single-key mappings")); |
| 42 | + }; |
| 43 | + if node.kind() != NodeKind::Map { |
| 44 | + return Err(site.error("audits must be strings or single-key mappings")); |
| 45 | + } |
| 46 | + let Some(definition) = non_blank_text(definition_name)? else { |
| 47 | + return Err(site.error("audit names must be non-empty strings")); |
| 48 | + }; |
| 49 | + check_identity(&definition, &definition_kind, site.path)?; |
| 50 | + match arguments.kind() { |
| 51 | + NodeKind::Null => Ok(bare_audit(definition_name.clone())), |
| 52 | + NodeKind::Map => configured_audit(definition_name, &definition, &arguments.entries(), site), |
| 53 | + _ => Err(site.error(&format!("audit '{definition}' arguments must be a mapping"))), |
40 | 54 | } |
41 | 55 | } |
42 | 56 |
|
43 | 57 | fn configured_audit<N: AuthoredNode>( |
44 | 58 | definition_name: &N, |
| 59 | + definition: &str, |
45 | 60 | options: &[(N, N)], |
46 | | -) -> Result<ParsedAudit<N>, HeaderMetadataDeferral> { |
47 | | - let name = optional_text(entry(options, "name"))?; |
| 61 | + site: Site<'_>, |
| 62 | +) -> Result<ParsedAudit<N>, HeaderMetadataStop> { |
| 63 | + let option_label = format!("{} audit '{definition}'", site.label); |
| 64 | + let option_site = Site { |
| 65 | + path: site.path, |
| 66 | + label: &option_label, |
| 67 | + }; |
| 68 | + let name = optional_text(entry(options, "name"), option_site, "name")?; |
48 | 69 | if let Some(name) = &name { |
49 | | - identity(name)?; |
| 70 | + let text = name.text().ok_or(HeaderMetadataStop::Unsupported)?; |
| 71 | + check_identity(&text, &format!("{} audit instance", site.label), site.path)?; |
50 | 72 | } |
51 | | - let description = optional_text(entry(options, "description"))?; |
52 | | - let severity = optional_text(entry(options, "severity"))?; |
53 | | - if let Some(severity) = &severity { |
54 | | - let text = severity.text().ok_or(HeaderMetadataDeferral::Unsupported)?; |
55 | | - if !AUDIT_SEVERITIES.contains(&text.as_str()) { |
56 | | - return Err(HeaderMetadataDeferral::Invalid); |
57 | | - } |
| 73 | + let description = optional_text(entry(options, "description"), option_site, "description")?; |
| 74 | + let severity = optional_text(entry(options, "severity"), option_site, "severity")?; |
| 75 | + if let Some(severity) = &severity |
| 76 | + && !AUDIT_SEVERITIES.iter().any(|value| severity.is_text(value)) |
| 77 | + { |
| 78 | + return Err(option_site.error(&format!( |
| 79 | + "'severity' must be one of: {}", |
| 80 | + AUDIT_SEVERITIES.join(", ") |
| 81 | + ))); |
58 | 82 | } |
59 | | - let run_scope = optional_text(entry(options, "run_scope"))?; |
60 | | - let always_run = optional_bool(entry(options, "always_run"))?; |
| 83 | + let run_scope = optional_text(entry(options, "run_scope"), option_site, "run_scope")?; |
| 84 | + let always_run = optional_bool(entry(options, "always_run"), option_site, "always_run")?; |
61 | 85 | if entry(options, "thresholds").is_some_and(|value| value.kind() != NodeKind::Null) { |
62 | | - return Err(HeaderMetadataDeferral::Unsupported); |
| 86 | + return Err(HeaderMetadataStop::Unsupported); |
63 | 87 | } |
64 | | - let minimum_samples = optional_count(entry(options, "minimum_samples"))?; |
65 | | - let evidence_limit = optional_count(entry(options, "evidence_limit"))?; |
| 88 | + let minimum_samples = optional_count( |
| 89 | + entry(options, "minimum_samples"), |
| 90 | + option_site, |
| 91 | + "minimum_samples", |
| 92 | + )?; |
| 93 | + let evidence_limit = optional_count( |
| 94 | + entry(options, "evidence_limit"), |
| 95 | + option_site, |
| 96 | + "evidence_limit", |
| 97 | + )?; |
66 | 98 | let arguments: Vec<(N, N)> = options |
67 | 99 | .iter() |
68 | 100 | .filter(|(key, _)| !is_option_key(key)) |
@@ -95,14 +127,6 @@ fn bare_audit<N: AuthoredNode>(definition_name: N) -> ParsedAudit<N> { |
95 | 127 | } |
96 | 128 | } |
97 | 129 |
|
98 | | -fn identity<N: AuthoredNode>(node: &N) -> Result<(), HeaderMetadataDeferral> { |
99 | | - if is_snake_case(&non_blank_text(node)?) { |
100 | | - Ok(()) |
101 | | - } else { |
102 | | - Err(HeaderMetadataDeferral::Invalid) |
103 | | - } |
104 | | -} |
105 | | - |
106 | 130 | fn is_option_key<N: AuthoredNode>(key: &N) -> bool { |
107 | 131 | AUDIT_OPTION_KEYS.iter().any(|name| key.is_text(name)) |
108 | 132 | } |
0 commit comments