Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! The first config field, in Python's walk order, whose string holds a macro call.
use crate::config_presence::_helpers::macro_calls::macro_call;
use crate::config_presence::models::{MacroPath, Presence};
use crate::types::{AuthoredNode, NodeKind};

/// Walk config values as `validate_no_macros_in_config_value` does, skipping the `skipped` keys.
pub fn first_macro_path<N: AuthoredNode>(values: &N, skipped: &[&str]) -> MacroPath {
let mut entries = values.entries();
entries.retain(|(key, _)| !skipped.iter().any(|name| key.is_text(name)));
walk_entries(&entries, &[])
}

fn walk<N: AuthoredNode>(node: &N, path: &[String]) -> MacroPath {
match node.kind() {
NodeKind::Str => match node.with_text(macro_call) {
Some(Presence::Present) => MacroPath::Found(path.to_vec()),
Some(Presence::Absent) => MacroPath::Absent,
Some(Presence::Deferred) | None => MacroPath::Deferred,
},
NodeKind::Map => walk_entries(&node.entries(), path),
NodeKind::List | NodeKind::Tuple => {
for item in node.items() {
let found = walk(&item, path);
if found != MacroPath::Absent {
return found;
}
}
MacroPath::Absent
}
_ => MacroPath::Absent,
}
}

fn walk_entries<N: AuthoredNode>(entries: &[(N, N)], path: &[String]) -> MacroPath {
for (key, value) in entries {
if key.kind() != NodeKind::Str {
continue;
}
let Some(name) = key.text() else {
return MacroPath::Deferred;
};
let mut child: Vec<String> = path.to_vec();
child.push(name);
let found = walk(value, &child);
if found != MacroPath::Absent {
return found;
}
}
MacroPath::Absent
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod contains_macro_call;
pub mod contains_template;
pub mod first_macro_path;
10 changes: 10 additions & 0 deletions crates/sqlbuild-model-config/src/config_presence/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ pub enum Presence {
/// A string needs Python's Unicode `\s` class to decide.
Deferred,
}

/// Where Python's config walk first finds a macro call.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MacroPath {
/// The mapping keys leading to the first string holding a macro call.
Found(Vec<String>),
Absent,
/// A string before any match needs Python's Unicode rules to decide.
Deferred,
}
69 changes: 69 additions & 0 deletions crates/sqlbuild-model-config/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Errors the native config stages report exactly as the Python stages raise them.
/// The Python exception class one error raises.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErrorClass {
/// `CompileInputError`.
CompileInput,
/// `ConfigValueTypeError` for a config key that must hold a string.
ConfigValueType,
/// `ResourceIdentityError` for an identity that is not canonical snake_case.
ResourceIdentity,
/// `DiscoveryConflictError` for equally specific path-default keys.
DiscoveryConflict,
}

/// One error with the class, message, code and help the Python stage raises.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfigError {
pub class: ErrorClass,
/// The message; empty for `ConfigValueTypeError`, which Python words from the value's type.
pub message: String,
/// The diagnostic code when it is not the class default.
pub code: Option<&'static str>,
pub help: Option<String>,
/// The config key a `ConfigValueTypeError` names.
pub key: Option<String>,
}

impl ConfigError {
/// A `CompileInputError` with this message.
pub fn compile(message: String) -> Self {
Self {
class: ErrorClass::CompileInput,
message,
code: None,
help: None,
key: None,
}
}

/// The `ConfigValueTypeError` `get_config_str` raises for a non-string `key`.
pub fn config_value_type(key: &str) -> Self {
Self {
class: ErrorClass::ConfigValueType,
message: String::new(),
code: None,
help: None,
key: Some(key.to_owned()),
}
}

/// This error of another class.
pub fn with_class(mut self, class: ErrorClass) -> Self {
self.class = class;
self
}

/// This error with help text.
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}

/// This error with a diagnostic code.
pub fn with_code(mut self, code: &'static str) -> Self {
self.code = Some(code);
self
}
}
118 changes: 71 additions & 47 deletions crates/sqlbuild-model-config/src/header_metadata/_helpers/audits.rs
Original file line number Diff line number Diff line change
@@ -1,68 +1,100 @@
//! Audit lists and instances, as `parse_audit_instances` reads them with `null_as_empty`.
use crate::header_metadata::_helpers::identity::check_identity;
use crate::header_metadata::_helpers::text::{
entry, is_snake_case, non_blank_text, optional_bool, optional_count, optional_text,
Site, entry, non_blank_text, optional_bool, optional_count, optional_text,
};
use crate::header_metadata::constants::{AUDIT_OPTION_KEYS, AUDIT_SEVERITIES};
use crate::header_metadata::models::{HeaderMetadataDeferral, ParsedAudit};
use crate::header_metadata::models::{HeaderMetadataStop, ParsedAudit};
use crate::types::{AuthoredNode, NodeKind};

/// Parse an authored audit list; Python's `None` is an empty list.
pub(crate) fn audit_list<N: AuthoredNode>(
node: &N,
) -> Result<Vec<ParsedAudit<N>>, HeaderMetadataDeferral> {
site: Site<'_>,
) -> Result<Vec<ParsedAudit<N>>, HeaderMetadataStop> {
match node.kind() {
NodeKind::Null => Ok(Vec::new()),
NodeKind::List => node.items().iter().map(audit_instance).collect(),
_ => Err(HeaderMetadataDeferral::Invalid),
NodeKind::List => node
.items()
.iter()
.map(|item| audit_instance(item, site))
.collect(),
_ => Err(site.error("audits must be a list")),
}
}

fn audit_instance<N: AuthoredNode>(node: &N) -> Result<ParsedAudit<N>, HeaderMetadataDeferral> {
match node.kind() {
NodeKind::Str => {
identity(node)?;
Ok(bare_audit(node.clone()))
}
NodeKind::Map => {
let entries = node.entries();
let [(definition_name, arguments)] = entries.as_slice() else {
return Err(HeaderMetadataDeferral::Invalid);
};
identity(definition_name)?;
match arguments.kind() {
NodeKind::Null => Ok(bare_audit(definition_name.clone())),
NodeKind::Map => configured_audit(definition_name, &arguments.entries()),
_ => Err(HeaderMetadataDeferral::Invalid),
}
}
_ => Err(HeaderMetadataDeferral::Invalid),
fn audit_instance<N: AuthoredNode>(
node: &N,
site: Site<'_>,
) -> Result<ParsedAudit<N>, HeaderMetadataStop> {
let definition_kind = format!("{} audit definition", site.label);
if node.kind() == NodeKind::Str {
let Some(text) = non_blank_text(node)? else {
return Err(site.error("audits must not contain empty names"));
};
check_identity(&text, &definition_kind, site.path)?;
return Ok(bare_audit(node.clone()));
}
let entries = node.entries();
let [(definition_name, arguments)] = entries.as_slice() else {
return Err(site.error("audits must be strings or single-key mappings"));
};
if node.kind() != NodeKind::Map {
return Err(site.error("audits must be strings or single-key mappings"));
}
let Some(definition) = non_blank_text(definition_name)? else {
return Err(site.error("audit names must be non-empty strings"));
};
check_identity(&definition, &definition_kind, site.path)?;
match arguments.kind() {
NodeKind::Null => Ok(bare_audit(definition_name.clone())),
NodeKind::Map => configured_audit(definition_name, &definition, &arguments.entries(), site),
_ => Err(site.error(&format!("audit '{definition}' arguments must be a mapping"))),
}
}

fn configured_audit<N: AuthoredNode>(
definition_name: &N,
definition: &str,
options: &[(N, N)],
) -> Result<ParsedAudit<N>, HeaderMetadataDeferral> {
let name = optional_text(entry(options, "name"))?;
site: Site<'_>,
) -> Result<ParsedAudit<N>, HeaderMetadataStop> {
let option_label = format!("{} audit '{definition}'", site.label);
let option_site = Site {
path: site.path,
label: &option_label,
};
let name = optional_text(entry(options, "name"), option_site, "name")?;
if let Some(name) = &name {
identity(name)?;
let text = name.text().ok_or(HeaderMetadataStop::Unsupported)?;
check_identity(&text, &format!("{} audit instance", site.label), site.path)?;
}
let description = optional_text(entry(options, "description"))?;
let severity = optional_text(entry(options, "severity"))?;
if let Some(severity) = &severity {
let text = severity.text().ok_or(HeaderMetadataDeferral::Unsupported)?;
if !AUDIT_SEVERITIES.contains(&text.as_str()) {
return Err(HeaderMetadataDeferral::Invalid);
}
let description = optional_text(entry(options, "description"), option_site, "description")?;
let severity = optional_text(entry(options, "severity"), option_site, "severity")?;
if let Some(severity) = &severity
&& !AUDIT_SEVERITIES.iter().any(|value| severity.is_text(value))
{
return Err(option_site.error(&format!(
"'severity' must be one of: {}",
AUDIT_SEVERITIES.join(", ")
)));
}
let run_scope = optional_text(entry(options, "run_scope"))?;
let always_run = optional_bool(entry(options, "always_run"))?;
let run_scope = optional_text(entry(options, "run_scope"), option_site, "run_scope")?;
let always_run = optional_bool(entry(options, "always_run"), option_site, "always_run")?;
if entry(options, "thresholds").is_some_and(|value| value.kind() != NodeKind::Null) {
return Err(HeaderMetadataDeferral::Unsupported);
return Err(HeaderMetadataStop::Unsupported);
}
let minimum_samples = optional_count(entry(options, "minimum_samples"))?;
let evidence_limit = optional_count(entry(options, "evidence_limit"))?;
let minimum_samples = optional_count(
entry(options, "minimum_samples"),
option_site,
"minimum_samples",
)?;
let evidence_limit = optional_count(
entry(options, "evidence_limit"),
option_site,
"evidence_limit",
)?;
let arguments: Vec<(N, N)> = options
.iter()
.filter(|(key, _)| !is_option_key(key))
Expand Down Expand Up @@ -95,14 +127,6 @@ fn bare_audit<N: AuthoredNode>(definition_name: N) -> ParsedAudit<N> {
}
}

fn identity<N: AuthoredNode>(node: &N) -> Result<(), HeaderMetadataDeferral> {
if is_snake_case(&non_blank_text(node)?) {
Ok(())
} else {
Err(HeaderMetadataDeferral::Invalid)
}
}

fn is_option_key<N: AuthoredNode>(key: &N) -> bool {
AUDIT_OPTION_KEYS.iter().any(|name| key.is_text(name))
}
Loading
Loading