Portable patterns
The small regular-expression language that means the same in every engine, and the one rule that keeps it fast.
matches tests a string against a pattern. Regular-expression engines disagree in small ways, so
Rule Cascade accepts only a portable subset and gives it one meaning in every language. The
search is unanchored: use ^ and $ to match the whole string. Matching is case-sensitive.
The pattern must be a string literal in the ruleset. A pattern outside the subset fails to load
with PATTERN_NOT_PORTABLE.
Syntax
| Syntax | Meaning |
|---|---|
abc | The characters themselves |
. | Any one character, line breaks included |
^, $ | The very start and the very end of the string |
\d \D \w \W | ASCII digits [0-9], word characters [A-Za-z0-9_], and their opposites |
\t \n \r | Tab, line feed, carriage return |
\. \( \$ ... | An escaped syntax character |
[a-z], [^0-9] | A character class, or its opposite |
(...), (?:...) | A group |
a|b | Either side |
* + ? {n} {n,} {n,m} | Repetition, at most 1000; add ? to make it lazy |
assert: { op: matches, args: [{ var: data.postcode }, '^\d{5}$'] }Example
A postcode is five digits. 75OO1 has two letter Os, so it is denied.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.patterns, version: 1.0.0, title: Portable patterns }
scope:
- { level: organization, id: learn }
entities:
Customer:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
- id: customer.postcode.format
kind: validation
target: { entity: Customer, field: /postcode }
operations: [create]
when: { op: exists, args: [{ var: data.postcode }] }
assert: { op: matches, args: [{ var: data.postcode }, '^\d{5}$'] }
severity: error
finding: { code: LRN-PAT-001, message: customer.badPostcode }
- id: customer.username.format
kind: validation
target: { entity: Customer, field: /username }
operations: [create]
when: { op: exists, args: [{ var: data.username }] }
assert: { op: matches, args: [{ var: data.username }, '^[a-z][a-z0-9_]{2,15}$'] }
severity: error
finding: { code: LRN-PAT-002, message: customer.badUsername }
messages:
en:
customer.badPostcode: "Enter a postcode of 5 digits."
customer.badUsername: "A username is 3 to 16 lower-case letters, digits or _, starting with a letter."
tests:
- name: a postcode with letters is denied
entity: Customer
operation: create
given:
data: { postcode: "75OO1", username: "ana_23" }
expect:
decision: deny
findings:
- { rule: customer.postcode.format, fields: [/postcode] }
- name: matching is case-sensitive
entity: Customer
operation: create
given:
data: { postcode: "75001", username: "Ana" }
expect:
decision: deny
findings:
- { rule: customer.username.format }
- name: good values are allowed
entity: Customer
operation: create
given:
data: { postcode: "75001", username: "ana_23" }
expect: { decision: allow, findings: [] }{
"entity": "Customer",
"operation": "create",
"data": {
"postcode": "75OO1",
"username": "ana_23"
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-PAT-001errorblockingEnter a postcode of 5 digits./postcode
Common mistakes
- Using
\s,\b,\p{...}, lookarounds or back-references. They are not portable. Write the characters out, for example[ \t]instead of\s. - Forgetting
^and$.\d{5}also matchesabc123456xyz. - Nesting repetitions without a bound. A group that repeats must not contain
*,+or{n,}at any depth.(a+)+can make a backtracking engine take exponential time to fail, so the compiler refuses it. The usual rewrite separates the repeated part by its first character:^[a-z]+(\.[a-z]+)*$becomes^[a-z](?:[a-z]|\.[a-z])*$.
The subject of matches may have at most 10000 characters. A longer one is an evaluation error.
This ruleset uses the nested form, so it does not load:
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.patterns-nested, version: 1.0.0, title: A nested quantifier }
scope:
- { level: organization, id: learn }
entities:
Customer:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
- id: customer.username.dotted
kind: validation
target: { entity: Customer, field: /username }
operations: [create]
when: { op: exists, args: [{ var: data.username }] }
assert: { op: matches, args: [{ var: data.username }, '^[a-z]+(\.[a-z]+)*$'] }
severity: error
finding: { code: LRN-PAT-010, message: customer.badUsername }
messages:
en:
customer.badUsername: "A username is lower-case words joined by dots, like ana.maria."
tests:
- name: a dotted username is allowed
entity: Customer
operation: create
given:
data: { username: "ana.maria" }
expect: { decision: allow, findings: [] }
- name: two dots in a row are denied
entity: Customer
operation: create
given:
data: { username: "ana..maria" }
expect:
decision: deny
findings:
- { rule: customer.username.dotted }{
"entity": "Customer",
"operation": "create",
"data": {
"username": "ana.maria"
}
}Result, from the engine
does not loadThe engine refuses the ruleset before it evaluates anything.
PATTERN_NOT_PORTABLErule: pattern "^[a-z]+(\\.[a-z]+)*$": a group that repeats must not contain an unbounded quantifier (*, + or {n,}) (line 15)
Exercise
Fix the username pattern so that it loads. ana.maria must stay allowed and ana..maria denied.
Hint
Use the rewrite above: one letter first, then letters or a dot followed by a letter.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.patterns-nested, version: 1.0.0, title: A nested quantifier }
scope:
- { level: organization, id: learn }
entities:
Customer:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
- id: customer.username.dotted
kind: validation
target: { entity: Customer, field: /username }
operations: [create]
when: { op: exists, args: [{ var: data.username }] }
assert: { op: matches, args: [{ var: data.username }, '^[a-z](?:[a-z]|\.[a-z])*$'] }
severity: error
finding: { code: LRN-PAT-010, message: customer.badUsername }
messages:
en:
customer.badUsername: "A username is lower-case words joined by dots, like ana.maria."
tests:
- name: a dotted username is allowed
entity: Customer
operation: create
given:
data: { username: "ana.maria" }
expect: { decision: allow, findings: [] }
- name: two dots in a row are denied
entity: Customer
operation: create
given:
data: { username: "ana..maria" }
expect:
decision: deny
findings:
- { rule: customer.username.dotted }{
"entity": "Customer",
"operation": "create",
"data": {
"username": "ana.maria"
}
}Result, from the engine
Decisionallow0 findings, server channel
Exercise
Change the postcode rule so that it also accepts US ZIP+4 codes like 12345-6789, but not
12345-67.
Hint
An optional group is (...)? and is allowed: it repeats at most once.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.patterns, version: 1.0.0, title: Portable patterns }
scope:
- { level: organization, id: learn }
entities:
Customer:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
- id: customer.postcode.format
kind: validation
target: { entity: Customer, field: /postcode }
operations: [create]
when: { op: exists, args: [{ var: data.postcode }] }
assert: { op: matches, args: [{ var: data.postcode }, '^\d{5}(-\d{4})?$'] }
severity: error
finding: { code: LRN-PAT-001, message: customer.badPostcode }
- id: customer.username.format
kind: validation
target: { entity: Customer, field: /username }
operations: [create]
when: { op: exists, args: [{ var: data.username }] }
assert: { op: matches, args: [{ var: data.username }, '^[a-z][a-z0-9_]{2,15}$'] }
severity: error
finding: { code: LRN-PAT-002, message: customer.badUsername }
messages:
en:
customer.badPostcode: "Enter a postcode of 5 digits, or 5+4 digits like 12345-6789."
customer.badUsername: "A username is 3 to 16 lower-case letters, digits or _, starting with a letter."
tests:
- name: a ZIP+4 postcode is allowed
entity: Customer
operation: create
given:
data: { postcode: "12345-6789" }
expect: { decision: allow, findings: [] }
- name: a half suffix is denied
entity: Customer
operation: create
given:
data: { postcode: "12345-67" }
expect:
decision: deny
findings:
- { rule: customer.postcode.format }{
"entity": "Customer",
"operation": "create",
"data": {
"postcode": "12345-6789"
}
}Result, from the engine
Decisionallow0 findings, server channel