use serde_json::Value;
use crate::ir::{Arena, NodeId};
use crate::profile::{Profile, Severity};
use crate::rules::class_b::helpers::missing_required_properties;
use crate::rules::metadata::{RuleCategory, RuleMetadata};
use crate::rules::registry::{Diagnostic, DiagnosticSeverity, Rule};
#[derive(Debug, Clone)]
pub(super) struct MaxDepthRule {
pub(super) limit: u32,
pub(super) profile_name: String,
}
impl Rule for MaxDepthRule {
fn check(&self, node: NodeId, arena: &Arena, profile: &Profile) -> Vec<Diagnostic> {
if arena[node].depth <= self.limit {
return Vec::new();
}
vec![Diagnostic {
code: format!("{}-S-max-depth", profile.code_prefix),
severity: DiagnosticSeverity::Error,
message: format!(
"object nesting depth {} exceeds limit of {}",
arena[node].depth, self.limit
),
pointer: arena[node].json_pointer.clone(),
source: None,
profile: self.profile_name.clone(),
hint: None,
}]
}
fn metadata(&self) -> Option<RuleMetadata> {
Some(RuleMetadata {
name: "max-depth".into(),
code: "{prefix}-S-max-depth".into(),
description: format!(
"Object nesting depth must not exceed {} levels",
self.limit
),
rationale: format!(
"{} limits object nesting depth to {} levels.",
self.profile_name, self.limit
),
severity: Severity::Forbid,
category: RuleCategory::Structural,
bad_example: "{ \"type\": \"object\", \"properties\": { \"nested\": { \"type\": \"object\", \"properties\": { \"too_deep\": { \"type\": \"object\" } } } } }".into(),
good_example: r#"{ "type": "object", "properties": { "name": { "type": "string" } } }"#.into(),
see_also: Vec::new(),
profile: Some(self.profile_name.clone()),
})
}
}
#[derive(Debug)]
struct BudgetRuleData {
code_suffix: &'static str,
message_label: &'static str,
hint: Option<&'static str>,
desc_subject: &'static str,
rationale_tail: &'static str,
bad_example: &'static str,
good_example: &'static str,
counter: fn(&Arena) -> usize,
}
#[derive(Debug, Clone)]
pub(super) struct BudgetRule {
limit: u32,
profile_name: String,
data: &'static BudgetRuleData,
}
static MAX_TOTAL_PROPERTIES: BudgetRuleData = BudgetRuleData {
code_suffix: "max-total-properties",
message_label: "total property count",
hint: None,
desc_subject: "Total object properties",
rationale_tail: "limits the total number of object properties.",
bad_example: "{ \"type\": \"object\", \"properties\": { \"...many\": {} } }",
good_example: r#"{ "type": "object", "properties": { "name": { "type": "string" } } }"#,
counter: count_total_properties,
};
static MAX_TOTAL_ENUM_VALUES: BudgetRuleData = BudgetRuleData {
code_suffix: "max-enum-values",
message_label: "total enum value count",
hint: None,
desc_subject: "Total enum values",
rationale_tail: "limits total enum values.",
bad_example: "{ \"type\": \"string\", \"enum\": [\"...1000+ values\"] }",
good_example: r#"{ "type": "string", "enum": ["red", "green", "blue"] }"#,
counter: count_total_enum_values,
};
static MAX_STRING_LENGTH: BudgetRuleData = BudgetRuleData {
code_suffix: "string-length-budget",
message_label: "total string length",
hint: None,
desc_subject: "Total property and enum string length",
rationale_tail: "enforces a schema string-length budget.",
bad_example: "{ \"type\": \"object\", \"properties\": { \"very_long_property_name\": { \"type\": \"string\" } } }",
good_example: r#"{ "type": "object", "properties": { "name": { "type": "string" } } }"#,
counter: count_string_length,
};
static MAX_OPTIONAL_PROPERTIES: BudgetRuleData = BudgetRuleData {
code_suffix: "max-optional-properties",
message_label: "optional property count",
hint: Some("Mark more properties as required or split the schema"),
desc_subject: "Optional properties",
rationale_tail: "limits optional parameters across strict schemas.",
bad_example: r#"{ "type": "object", "properties": { "optional": { "type": "string" } } }"#,
good_example: r#"{ "type": "object", "properties": { "required": { "type": "string" } }, "required": ["required"], "additionalProperties": false }"#,
counter: count_optional_properties,
};
static MAX_UNION_PROPERTIES: BudgetRuleData = BudgetRuleData {
code_suffix: "max-union-properties",
message_label: "union parameter count",
hint: Some("Reduce anyOf/type-array usage or split the schema"),
desc_subject: "Union parameters",
rationale_tail: "limits parameters that use anyOf or type arrays across strict schemas.",
bad_example: r#"{ "type": "object", "properties": { "value": { "anyOf": [{ "type": "string" }, { "type": "number" }] } } }"#,
good_example: r#"{ "type": "object", "properties": { "value": { "type": "string" } }, "required": ["value"], "additionalProperties": false }"#,
counter: count_union_properties,
};
fn count_total_properties(arena: &Arena) -> usize {
arena
.iter()
.filter_map(|(_, n)| n.annotations.properties.as_ref())
.filter_map(|v| v.as_object().map(|o| o.len()))
.sum()
}
fn count_total_enum_values(arena: &Arena) -> usize {
arena
.iter()
.filter_map(|(_, n)| n.annotations.enum_values.as_ref())
.filter_map(|v| v.as_array().map(|a| a.len()))
.sum()
}
fn count_string_length(arena: &Arena) -> usize {
let property_names: usize = arena
.iter()
.filter_map(|(_, n)| n.annotations.properties.as_ref())
.filter_map(|v| v.as_object())
.flat_map(|props| props.keys())
.map(|k| k.len())
.sum();
let enum_strings: usize = arena
.iter()
.filter_map(|(_, n)| n.annotations.enum_values.as_ref())
.filter_map(|v| match v {
Value::Array(arr) => Some(arr),
_ => None,
})
.flatten()
.filter_map(|v| v.as_str())
.map(str::len)
.sum();
property_names + enum_strings
}
fn count_optional_properties(arena: &Arena) -> usize {
arena
.iter()
.map(|(_, node)| missing_required_properties(node).len())
.sum()
}
fn count_union_properties(arena: &Arena) -> usize {
arena
.iter()
.filter(|(_, node)| {
node.annotations.any_of.is_some()
|| matches!(
&node.annotations.r#type,
Some(Value::Array(types)) if types.len() > 1
)
})
.count()
}
impl Rule for BudgetRule {
fn check(&self, node: NodeId, arena: &Arena, profile: &Profile) -> Vec<Diagnostic> {
if arena[node].parent.is_some() {
return Vec::new();
}
let d = self.data;
let total = (d.counter)(arena);
if total <= self.limit as usize {
return Vec::new();
}
vec![Diagnostic {
code: format!("{}-S-{}", profile.code_prefix, d.code_suffix),
severity: DiagnosticSeverity::Error,
message: format!(
"{} {} exceeds limit of {}",
d.message_label, total, self.limit
),
pointer: String::new(),
source: None,
profile: self.profile_name.clone(),
hint: d.hint.map(str::to_owned),
}]
}
fn metadata(&self) -> Option<RuleMetadata> {
let d = self.data;
Some(RuleMetadata {
name: d.code_suffix.into(),
code: format!("{{prefix}}-S-{}", d.code_suffix),
description: format!("{} must not exceed {}", d.desc_subject, self.limit),
rationale: format!("{} {}", self.profile_name, d.rationale_tail),
severity: Severity::Forbid,
category: RuleCategory::Structural,
bad_example: d.bad_example.into(),
good_example: d.good_example.into(),
see_also: Vec::new(),
profile: Some(self.profile_name.clone()),
})
}
}
impl BudgetRule {
pub(super) fn max_total_properties(limit: u32, profile_name: String) -> Self {
Self {
limit,
profile_name,
data: &MAX_TOTAL_PROPERTIES,
}
}
pub(super) fn max_total_enum_values(limit: u32, profile_name: String) -> Self {
Self {
limit,
profile_name,
data: &MAX_TOTAL_ENUM_VALUES,
}
}
pub(super) fn max_string_length(limit: u32, profile_name: String) -> Self {
Self {
limit,
profile_name,
data: &MAX_STRING_LENGTH,
}
}
pub(super) fn max_optional_properties(limit: u32, profile_name: String) -> Self {
Self {
limit,
profile_name,
data: &MAX_OPTIONAL_PROPERTIES,
}
}
pub(super) fn max_union_properties(limit: u32, profile_name: String) -> Self {
Self {
limit,
profile_name,
data: &MAX_UNION_PROPERTIES,
}
}
}