Add optional regex constraints
This commit is contained in:
parent
84680e2652
commit
f1a5836247
47
Cargo.lock
generated
47
Cargo.lock
generated
|
|
@ -2,6 +2,15 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decodal"
|
||||
version = "0.1.0"
|
||||
|
|
@ -12,3 +21,41 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "decodal-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
|
|
|||
|
|
@ -3,5 +3,9 @@ name = "decodal"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
regex = ["decodal-core/regex"]
|
||||
|
||||
[dependencies]
|
||||
decodal-core = { path = "../decodal-core" }
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
doc/manual/souce/design/features.md
Normal file
34
doc/manual/souce/design/features.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Features
|
||||
|
||||
Decodal keeps the embedded core small by making heavy functionality opt-in.
|
||||
|
||||
## Core defaults
|
||||
|
||||
`decodal-core` defaults to `std` only.
|
||||
|
||||
```toml
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
||||
regex = ["std", "dep:regex"]
|
||||
```
|
||||
|
||||
Building `decodal-core` with `--no-default-features` keeps the core in `no_std + alloc` mode and avoids optional dependencies.
|
||||
|
||||
## Regex
|
||||
|
||||
Regex constraints are implemented behind the `regex` feature.
|
||||
When the feature is disabled, regex constraints parse and compose, but validating a concrete value against them returns an unsupported feature diagnostic.
|
||||
|
||||
```sh
|
||||
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
|
||||
```
|
||||
|
||||
Regex constraints are accumulated during `&` composition.
|
||||
The implementation does not try to prove whether the intersection of two regex constraints is empty.
|
||||
Concrete strings must match every regex constraint attached to the abstract value.
|
||||
|
||||
## CLI features
|
||||
|
||||
`decodal-cli` exposes a matching `regex` feature that enables `decodal-core/regex`.
|
||||
The feature is not enabled by default so the default CLI binary remains small.
|
||||
|
|
@ -16,3 +16,4 @@ bytecode VM や JIT ではなく、AST を demand-driven に評価すること
|
|||
4. [Composition and Materialization](./composition-and-materialization.md)
|
||||
5. [Diagnostics and Fallback](./diagnostics-and-fallback.md)
|
||||
6. [Embedding API](./embedding-api.md)
|
||||
7. [Features](./features.md)
|
||||
|
|
|
|||
|
|
@ -42,4 +42,5 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||
4. [Composition and Materialization](./design/composition-and-materialization.md)
|
||||
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
||||
6. [Embedding API](./design/embedding-api.md)
|
||||
7. [Features](./design/features.md)
|
||||
4. [Open Issues](./open-issues.md)
|
||||
|
|
|
|||
9
examples/regex/README.md
Normal file
9
examples/regex/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Regex example
|
||||
|
||||
Regex constraints are optional. Run this example with the `regex` feature enabled:
|
||||
|
||||
```sh
|
||||
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
|
||||
```
|
||||
|
||||
Without the feature, regex validation returns an unsupported feature diagnostic.
|
||||
7
examples/regex/main.dcdl
Normal file
7
examples/regex/main.dcdl
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
let
|
||||
Identifier = String & /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
ApiName = Identifier & /^api_/;
|
||||
in
|
||||
{
|
||||
service = ApiName default "api_gateway";
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user