Add optional regex constraints

This commit is contained in:
2026-06-16 11:27:27 +09:00
parent 84680e2652
commit f1a5836247
9 changed files with 160 additions and 6 deletions
+55 -5
View File
@@ -707,11 +707,7 @@ impl<L: SourceLoader> Engine<L> {
"value does not satisfy comparison constraint",
)
}),
Constraint::Regex(_) => Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
"regex constraints require a future regex feature",
)),
Constraint::Regex(pattern) => satisfies_regex(value, pattern, span),
Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
@@ -967,6 +963,42 @@ fn value_matches_primitive(value: &RuntimeValue, primitive: PrimitiveType) -> bo
)
}
#[cfg(feature = "regex")]
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
return Err(Diagnostic::new(
DiagnosticKind::ConstraintViolation,
span,
"regex constraints require a string value",
));
};
let regex = regex::Regex::new(pattern).map_err(|error| {
Diagnostic::new(
DiagnosticKind::Syntax,
span,
format!("invalid regex constraint `{pattern}`: {error}"),
)
})?;
if regex.is_match(value) {
Ok(())
} else {
Err(Diagnostic::new(
DiagnosticKind::ConstraintViolation,
span,
"value does not satisfy regex constraint",
))
}
}
#[cfg(not(feature = "regex"))]
fn satisfies_regex(_value: &RuntimeValue, _pattern: &str, span: Span) -> Result<()> {
Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
"regex constraints require the regex feature",
))
}
fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
match (value, expected) {
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
@@ -1169,4 +1201,22 @@ mod tests {
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[1].value, Data::String(String::from("local")));
}
#[cfg(feature = "regex")]
#[test]
fn regex_constraint_matches_concrete_string() {
let data = eval_data(r#"value = String & /^api-[0-9]+$/ default "api-123";"#);
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[0].value, Data::String(String::from("api-123")));
}
#[cfg(feature = "regex")]
#[test]
fn regex_constraint_rejects_non_matching_string() {
let parsed =
crate::parse_source(r#"value = String & /^api-[0-9]+$/ default "web-123";"#).unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
let value = engine.eval_root().unwrap();
assert!(engine.materialize(&value).is_err());
}
}