Add optional regex constraints
This commit is contained in:
@@ -6,6 +6,7 @@ edition = "2024"
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
||||
regex = []
|
||||
regex = ["std", "dep:regex"]
|
||||
|
||||
[dependencies]
|
||||
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user