String operators
len, lower, upper, trim, concat, substring, text, startsWith, endsWith and contains work with text.
String operators measure, change and search text. They count Unicode code points, so len of
"café" is 4 in every language. lower and upper change only the letters A to Z.
Syntax
{ op: len, args: [<string or list>] }
{ op: substring, args: [<string>, <start>, <length>] }
{ op: concat, args: [<string>, <string>, ...] }| Operator | Arguments | Result |
|---|---|---|
len | string or list | Number of code points, or of elements |
lower, upper | string | Only the ASCII letters A-Z / a-z change |
trim | string | Without leading and trailing space, tab, line feed and carriage return |
concat | any number of strings | The joined string; "" for no arguments |
substring | string, start, optional length | By code points, from 0. Ranges beyond the end are cut |
text | any | The value as text: 1500 becomes "1500" |
startsWith, endsWith, contains | string, string | Boolean |
Example
Ten rules check one customer. The request has an upper-case user name, a space before the name and a French VAT number for a Spanish customer.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.string, version: 1.0.0, title: String operators }
scope:
- { level: organization, id: learn }
entities:
Customer:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
- id: customer.username.length
kind: validation
target: { entity: Customer, field: /username }
operations: [create]
assert: { op: between, args: [{ op: len, args: [{ var: data.username }] }, 3, 20] }
severity: error
finding: { code: STR-001, message: username.length }
- id: customer.username.lower-case
kind: validation
target: { entity: Customer, field: /username }
operations: [create]
assert: { op: eq, args: [{ var: data.username }, { op: lower, args: [{ var: data.username }] }] }
severity: error
finding: { code: STR-002, message: username.lowerCase }
- id: customer.country.upper-case
kind: validation
target: { entity: Customer, field: /country }
operations: [create]
assert: { op: eq, args: [{ var: data.country }, { op: upper, args: [{ var: data.country }] }] }
severity: error
finding: { code: STR-003, message: country.upperCase }
- id: customer.name.trimmed
kind: validation
target: { entity: Customer, field: /name }
operations: [create]
assert: { op: eq, args: [{ var: data.name }, { op: trim, args: [{ var: data.name }] }] }
severity: error
finding: { code: STR-004, message: name.trimmed }
- id: customer.email.at-sign
kind: validation
target: { entity: Customer, field: /email }
operations: [create]
assert: { op: contains, args: [{ var: data.email }, "@"] }
severity: error
finding: { code: STR-005, message: email.atSign }
- id: customer.email.not-invalid
kind: validation
target: { entity: Customer, field: /email }
operations: [create]
assert: { op: not, args: [{ op: endsWith, args: [{ var: data.email }, ".invalid"] }] }
severity: error
finding: { code: STR-006, message: email.invalidDomain }
- id: customer.website.https
kind: validation
target: { entity: Customer, field: /website }
operations: [create]
assert: { op: startsWith, args: [{ var: data.website }, "https://"] }
severity: error
finding: { code: STR-007, message: website.https }
- id: customer.vat-id.country
kind: validation
target: { entity: Customer, field: /vatId }
operations: [create]
assert: { op: eq, args: [{ op: substring, args: [{ var: data.vatId }, 0, 2] }, { var: data.country }] }
severity: error
finding: { code: STR-008, message: vatId.country }
- id: customer.nickname.label
kind: validation
target: { entity: Customer, field: /nickname }
operations: [create]
assert: { op: eq, args: [{ var: data.nickname }, { op: concat, args: [{ var: data.name }, " (", { var: data.country }, ")"] }] }
severity: error
finding: { code: STR-009, message: nickname.label }
- id: customer.credit-limit.whole
kind: validation
target: { entity: Customer, field: /creditLimit }
operations: [create]
assert: { op: not, args: [{ op: contains, args: [{ op: text, args: [{ var: data.creditLimit }] }, "."] }] }
severity: error
finding: { code: STR-010, message: creditLimit.whole }
messages:
en:
username.length: "A user name has 3 to 20 characters."
username.lowerCase: "Write the user name in lower case."
country.upperCase: "Write the country code in upper case."
name.trimmed: "Remove the spaces around the name."
email.atSign: "An e-mail address contains @."
email.invalidDomain: "This e-mail domain does not exist."
website.https: "The website must start with https://."
vatId.country: "The VAT number must start with the country code."
nickname.label: "The nickname must be the name followed by the country in brackets."
creditLimit.whole: "The credit limit is a whole number."
tests:
- name: an upper-case user name, a padded name and a foreign VAT number
entity: Customer
operation: create
given:
data:
username: "AnaB"
country: "ES"
name: " Ana"
email: "ana@example.com"
website: "https://ana.example"
vatId: "FR123456789"
nickname: " Ana (ES)"
creditLimit: 1500
expect:
decision: deny
findings:
- { rule: customer.username.lower-case }
- { rule: customer.name.trimmed }
- { rule: customer.vat-id.country }
- name: a clean customer
entity: Customer
operation: create
given:
data:
username: "anab"
country: "ES"
name: "Ana"
email: "ana@example.com"
website: "https://ana.example"
vatId: "ES123456789"
nickname: "Ana (ES)"
creditLimit: 1500
expect: { decision: allow, findings: [] }
- name: a credit limit with a fraction
entity: Customer
operation: create
given:
data:
username: "anab"
country: "ES"
name: "Ana"
email: "ana@example.com"
website: "https://ana.example"
vatId: "ES123456789"
nickname: "Ana (ES)"
creditLimit: 1500.5
expect:
decision: deny
findings:
- { rule: customer.credit-limit.whole }{
"entity": "Customer",
"operation": "create",
"data": {
"username": "AnaB",
"country": "ES",
"name": " Ana",
"email": "ana@example.com",
"website": "https://ana.example",
"vatId": "FR123456789",
"nickname": " Ana (ES)",
"creditLimit": 1500
}
}Result, from the engine
Decisiondeny3 findings, server channel
STR-002errorblockingWrite the user name in lower case./usernameSTR-004errorblockingRemove the spaces around the name./nameSTR-008errorblockingThe VAT number must start with the country code./vatId
Common mistakes
concatjoins strings only. Turn a number into text first withtext.- String operators on a missing field are an evaluation error. Guard optional fields with
when. lowerandupperdo not change accented letters:upperof"é"is"é".
Exercise
A phone number must start with + and have at most 16 characters. Write two rules. Test
"0034612345678" (denied), "+34 612 345 678 90" (denied) and "+34612345678" (allowed).
Hint
Use startsWith with "+" for the first rule, and len with lte for the second.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.string, version: 1.0.0, title: String operators }
scope:
- { level: organization, id: learn }
entities:
Customer:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
- id: customer.phone.plus
kind: validation
target: { entity: Customer, field: /phone }
operations: [create]
assert: { op: startsWith, args: [{ var: data.phone }, "+"] }
severity: error
finding: { code: STR-001, message: phone.plus }
- id: customer.phone.length
kind: validation
target: { entity: Customer, field: /phone }
operations: [create]
assert: { op: lte, args: [{ op: len, args: [{ var: data.phone }] }, 16] }
severity: error
finding: { code: STR-002, message: phone.length }
messages:
en:
phone.plus: "Start the phone number with + and the country code."
phone.length: "A phone number has at most 16 characters."
tests:
- name: a phone number without the plus is denied
entity: Customer
operation: create
given:
data: { phone: "0034612345678" }
expect:
decision: deny
findings:
- { rule: customer.phone.plus, fields: [/phone] }
- name: a phone number that is too long is denied
entity: Customer
operation: create
given:
data: { phone: "+34 612 345 678 90" }
expect:
decision: deny
findings:
- { rule: customer.phone.length }
- name: an international phone number is allowed
entity: Customer
operation: create
given:
data: { phone: "+34612345678" }
expect: { decision: allow, findings: [] }{
"entity": "Customer",
"operation": "create",
"data": {
"phone": "0034612345678"
}
}Result, from the engine
Decisiondeny1 finding, server channel
STR-001errorblockingStart the phone number with + and the country code./phone