Creating a New Rule
This guide walks you through creating a new rule for dbtective.
Prerequisites
Before starting, determine whether your rule is a Manifest Rule or a Catalog Rule:
- Manifest rules use only
manifest.json- contains model definitions, configs, and metadata - Catalog rules use both
manifest.jsonandcatalog.json- includes actual database column information
See the rules overview or the dbt docs on manifest and catalog artifacts.
Creating a new rule
Here I explain how to add a new ManifestRule. For a catalog rule, add your rule to the CatalogSpecificRuleConfig enum in src/core/config/catalog_rule.rs and follow the same steps as above (with the compiler helping you along the way). It works almost identically.
Add the Rule Enum Variant
Add a new entry to the ManifestSpecificRuleConfig enum in src/core/config/manifest_rule.rs:
#[derive(Debug, Deserialize, EnumIter, AsRefStr, EnumString)]
#[strum(serialize_all = "snake_case")]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ManifestSpecificRuleConfig {
HasDescription {},
// ... existing rules ...
// Add your new rule here
YourRuleName {
your_field_name_for_the_rule: String,
},
}- Use
PascalCasefor the enum variant name - The
snake_caseserialization converts it automatically (e.g.,HasOwner→has_ownerin confor the user config) - Add rule-specific arguments inside the
{}braces
Configure Applies To
In the same file, update two functions:
default_applies_to_for_manifest_rule - Default targets when user doesn’t specify:
pub fn default_applies_to_for_manifest_rule(rule_type: &ManifestSpecificRuleConfig) -> AppliesTo {
match rule_type {
// ... existing rules ...
ManifestSpecificRuleConfig::HasOwner { .. } => AppliesTo {
node_objects: vec![RuleTarget::Models],
source_objects: vec![RuleTarget::Sources],
unit_test_objects: vec![],
macro_objects: vec![],
exposure_objects: vec![],
semantic_model_objects: vec![],
custom_objects: vec![],
},
}
}applies_to_options_for_manifest_rule - All valid targets users can choose:
fn applies_to_options_for_manifest_rule(rule_type: &ManifestSpecificRuleConfig) -> AppliesTo {
match rule_type {
// ... existing rules ...
ManifestSpecificRuleConfig::HasOwner { .. } => AppliesTo {
node_objects: vec![RuleTarget::Models, RuleTarget::Seeds, RuleTarget::Snapshots],
source_objects: vec![RuleTarget::Sources],
// ... etc
},
}
}Create or Find a Trait
Check src/core/rules/rule_config/ for an existing trait that matches your rule’s needs:
| Trait | Purpose | File |
|---|---|---|
Descriptable | Objects with descriptions | has_description.rs |
HasTags | Objects with tags | has_tags.rs |
HasMetadata | Objects with metadata | has_metadata_keys.rs |
Nameable | Objects with names | name_convention.rs |
If a suitable trait exists, add your function to it. If not, create a new file in src/core/rules/rule_config/.
If re-using a trait, we might need to rename some files. This is okay since the trait represents a broader concept.
All traits contain at least the following methods (needed for RuleResult (table reporting)):
fn get_object_type(&self) -> &str;- Returns the dbt object type (e.g., “model”, “source”)fn get_object_string(&self) -> &str;- Returns a string representationfn get_relative_path(&self) -> Option<&String>;- Returns the object’s relative file path
Implement the Rule Logic
Create your rule function. Here’s an example pattern:
// src/core/rules/rule_config/your_rule.rs
use crate::{
cli::table::RuleResult,
core::config::manifest_rule::ManifestRule,
};
/// Trait for objects that can have an owner
pub trait YourRule {
fn get_owner(&self) -> Option<&str>;
fn get_object_type(&self) -> &str;
fn get_object_string(&self) -> &str;
fn get_relative_path(&self) -> Option<&String>;
}
/// Check if an object has a valid owner configured
pub fn has_your_rule<T: YourRule>(
obj: &T,
rule: &ManifestRule,
your_field_name_for_the_rule: &str,
) -> Option<RuleResult> {
// Your rule logic here
}Implement the Trait for dbt Objects
Implement your trait for the relevant structs in src/core/manifest/dbt_objects/.
Don’t worry if you miss any, the Rust compiler will guide you, (so you can also skip this for now and move to step 6).
// In the appropriate dbt_objects file
impl YourRule for Node {
fn get_owner(&self) -> Option<&str> {
self.config.as_ref()?.meta.as_ref()?.get("owner")?.as_str()
}
fn get_object_type(&self) -> &str {
&self.resource_type
}
fn get_object_string(&self) -> &str {
&self.name
}
fn get_relative_path(&self) -> Option<&String> {
self.original_file_path.as_ref()
}
}Add the Rule in Node Rules
Add your rule to src/core/rules/manifest/node_rules.rs. If you haven’t implemented the trait yet, you will get a compile error prompting you to do so.
use crate::core::rules::rule_config::your_rule;
// In the apply_node_rules function, add to the match statement:
let row_result = match &rule.rule {
// ... existing rules ...
ManifestSpecificRuleConfig::YourRuleName {
your_field_name_for_the_rule,
allow_empty,
} => your_rule(node, rule, your_field_name_for_the_rule, *allow_empty),
};Similarly, update src/core/rules/manifest/apply_other_manifest_object_rules.rs.
If any object doesn’t apply to your rule, simply return the accumulator of ruleresults (acc) unchanged.
Export the Module
Add your new module to src/core/rules/rule_config/mod.rs:
mod your_rule;
pub use your_rule::{your_rule, YourRule};Write Unit Tests
Add tests in your rule file:
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::{manifest_rule::ManifestSpecificRuleConfig, severity::Severity};
struct TestObject {
your_field_name_for_the_rule: Option<String>,
}
impl YourRule for TestObject {
fn get_owner(&self) -> Option<&str> {
self.your_field_name_for_the_rule.as_deref()
}
fn get_object_type(&self) -> &str { "model" }
fn get_object_string(&self) -> &str { "test_model" }
fn get_relative_path(&self) -> Option<&String> { None }
}
#[test]
fn test_missing_owner() {
let obj = TestObject { your_field_name_for_the_rule: None };
let rule = create_test_rule();
let result = your_rule(&obj, &rule, "your_field_name_for_the_rule", false);
assert!(result.is_some());
assert!(result.unwrap().message.contains("some message e.g. missing owner"));
}
#[test]
fn test_valid_owner() {
let obj = TestObject { your_field_name_for_the_rule: Some("team-data".to_string()) };
let rule = create_test_rule();
let result = your_rule(&obj, &rule, "your_field_name_for_the_rule", false);
assert!(result.is_none());
}
}Write Integration Tests
Create tests in the tests/ folder. Copy the structure from existing tests and adapt it to your rule.
Document the Rule
Create documentation in docs/content/docs/rules/your_rule.md. Copy the structure from existing rule docs & fill in the details to fit your rule. Remember to include the applies_to options from the src/core/config/manifest_rule.rs file.
Tips
- The Rust compiler will guide you through missing implementations after you filled in the original Enum, so relax and take it step by step.
- Look at existing rules for patterns
- Ctrll+F on existing rules to show what needs to be updated.