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.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
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]]
|
[[package]]
|
||||||
name = "decodal"
|
name = "decodal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -12,3 +21,41 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-core"
|
name = "decodal-core"
|
||||||
version = "0.1.0"
|
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"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
regex = ["decodal-core/regex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal-core = { path = "../decodal-core" }
|
decodal-core = { path = "../decodal-core" }
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ edition = "2024"
|
||||||
[features]
|
[features]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
std = []
|
std = []
|
||||||
regex = []
|
regex = ["std", "dep:regex"]
|
||||||
|
|
||||||
[dependencies]
|
[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",
|
"value does not satisfy comparison constraint",
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
Constraint::Regex(_) => Err(Diagnostic::new(
|
Constraint::Regex(pattern) => satisfies_regex(value, pattern, span),
|
||||||
DiagnosticKind::UnsupportedFeature,
|
|
||||||
span,
|
|
||||||
"regex constraints require a future regex feature",
|
|
||||||
)),
|
|
||||||
Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
|
Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
|
||||||
DiagnosticKind::UnsupportedFeature,
|
DiagnosticKind::UnsupportedFeature,
|
||||||
span,
|
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 {
|
fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
|
||||||
match (value, expected) {
|
match (value, expected) {
|
||||||
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
|
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
|
||||||
|
|
@ -1169,4 +1201,22 @@ mod tests {
|
||||||
let Data::Object(fields) = data else { panic!() };
|
let Data::Object(fields) = data else { panic!() };
|
||||||
assert_eq!(fields[1].value, Data::String(String::from("local")));
|
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)
|
4. [Composition and Materialization](./composition-and-materialization.md)
|
||||||
5. [Diagnostics and Fallback](./diagnostics-and-fallback.md)
|
5. [Diagnostics and Fallback](./diagnostics-and-fallback.md)
|
||||||
6. [Embedding API](./embedding-api.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)
|
4. [Composition and Materialization](./design/composition-and-materialization.md)
|
||||||
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
||||||
6. [Embedding API](./design/embedding-api.md)
|
6. [Embedding API](./design/embedding-api.md)
|
||||||
|
7. [Features](./design/features.md)
|
||||||
4. [Open Issues](./open-issues.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