///|
/// Defensive configuration limits for rule sets loaded from external JSON.
pub(all) struct RuleSetLimits {
max_rules : Int
max_name_length : Int
max_message_length : Int
max_total_source_length : Int
require_unique_names : Bool
compile_limits : CompileLimits
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn RuleSetLimits::default() -> RuleSetLimits {
{
max_rules: 512,
max_name_length: 128,
max_message_length: 2048,
max_total_source_length: 262144,
require_unique_names: true,
compile_limits: CompileLimits::default(),
}
}
///|
fn validate_ruleset_limits(limits : RuleSetLimits) -> Result[Unit, Diagnostic] {
if limits.max_rules <= 0 ||
limits.max_name_length <= 0 ||
limits.max_message_length <= 0 ||
limits.max_total_source_length <= 0 {
Err(
Diagnostic::new(
Configure,
"C019",
"rule-set limits must be positive integers",
Span::new(0, 0),
hint="Set every numeric RuleSetLimits field to at least 1.",
),
)
} else {
validate_compile_limits(limits.compile_limits)
}
}
///|
fn rule_configuration_diagnostic(
code : String,
rule_name : String,
message : String,
hint? : String,
) -> Diagnostic {
Diagnostic::new(
Configure,
code,
if rule_name.is_empty() {
message
} else {
"rule `\{rule_name}`: \{message}"
},
Span::new(0, 0),
hint?,
)
}
///|
fn validate_rule_definition(
definition : RuleDefinition,
limits : RuleSetLimits,
seen_names : Array[String],
) -> Array[Diagnostic] {
let diagnostics = []
if definition.name.is_empty() {
diagnostics.push(
rule_configuration_diagnostic(
"C021",
"",
"rule name must not be empty",
hint="Give each rule a stable, descriptive identifier.",
),
)
} else if definition.name.length() > limits.max_name_length {
diagnostics.push(
rule_configuration_diagnostic(
"C022",
definition.name,
"rule name exceeds the configured length limit",
hint="Shorten the identifier or explicitly raise max_name_length.",
),
)
}
if definition.expression.is_empty() {
diagnostics.push(
rule_configuration_diagnostic(
"C023",
definition.name,
"rule expression must not be empty",
hint="Provide a boolean MoonRule expression.",
),
)
}
if definition.message.length() > limits.max_message_length {
diagnostics.push(
rule_configuration_diagnostic(
"C024",
definition.name,
"rule message exceeds the configured length limit",
hint="Keep validation messages concise or raise max_message_length.",
),
)
}
if limits.require_unique_names && seen_names.contains(definition.name) {
diagnostics.push(
rule_configuration_diagnostic(
"C025",
definition.name,
"duplicate rule name",
hint="Use unique names so reports and metrics can identify each rule.",
),
)
}
diagnostics
}
///|
/// Compile a rule set with size, naming, and per-expression safety limits.
pub fn RuleSet::compile_with_limits(
definitions : Array[RuleDefinition],
limits : RuleSetLimits,
) -> Result[RuleSet, Array[Diagnostic]] {
match validate_ruleset_limits(limits) {
Err(diagnostic) => return Err([diagnostic])
Ok(_) => ()
}
if definitions.length() > limits.max_rules {
return Err([
Diagnostic::new(
Configure,
"C020",
"rule set contains \{definitions.length()} rules, exceeding the configured limit of \{limits.max_rules}",
Span::new(0, 0),
hint="Split the configuration or explicitly raise max_rules.",
),
])
}
let mut total_source_length = 0
for definition in definitions {
total_source_length = total_source_length + definition.expression.length()
}
if total_source_length > limits.max_total_source_length {
return Err([
Diagnostic::new(
Configure,
"C026",
"combined rule source exceeds the configured length limit",
Span::new(0, 0),
hint="Split the rule set or explicitly raise max_total_source_length.",
),
])
}
let rules = []
let diagnostics = []
let seen_names = []
for definition in definitions {
let definition_diagnostics = validate_rule_definition(
definition, limits, seen_names,
)
for diagnostic in definition_diagnostics {
diagnostics.push(diagnostic)
}
if !definition.name.is_empty() {
seen_names.push(definition.name)
}
if !definition.expression.is_empty() {
match compile_with_limits(definition.expression, limits.compile_limits) {
Ok(program) => rules.push({ definition, program })
Err(diagnostic) =>
diagnostics.push({
..diagnostic,
message: "rule `\{definition.name}`: \{diagnostic.message}",
})
}
}
}
if diagnostics.is_empty() {
Ok({ rules, })
} else {
Err(diagnostics)
}
}
///|
/// Decode and compile rule definitions using explicit configuration limits.
pub fn RuleSet::from_json_with_limits(
config : Json,
limits : RuleSetLimits,
) -> Result[RuleSet, Array[Diagnostic]] {
let definitions : Array[RuleDefinition] = @json.from_json(config) catch {
error =>
return Err([
Diagnostic::new(
Configure,
"C001",
"invalid rule configuration: \{error}",
Span::new(0, 0),
hint="Expected a JSON array containing name, expression, message, and severity.",
),
])
}
RuleSet::compile_with_limits(definitions, limits)
}