Verifications
Problematic
In most projects, if not all, we need to define some input validations. Also, sometimes, we want to express some invariants in our models.
There is a lot of means to define these validations. Here some cases in different languages that could be found in projects:
def nonEmptyString(value: String): Validated[String] = {
if (value.nonEmpty) {
Valid(value)
} else {
Invalid("The string should not be empty")
}
}
// If you do not know TypeScript, consider it as JavaScript with types only.
function isNonEmptyString(value: string): boolean {
return value !== undefined && value !== null && value !== ''
}
function validateNonEmptyString(String $value) {
if ($value == null || empty($value)) {
throw new InvalidArgumentException("The string should not be empty");
}
}
class NonEmptyStringVerification extends Verification<String> {
public String getErrorMessage() {
return "The string should not be empty";
}
public boolean validate(String value) {
return value != null && !value.isEmpty();
}
}
Each previous variant do the same thing: validating that a string is non empty (with language specific features sometimes). We usually have the same thing as input (the string value to validate) but several output types (Validation type, boolean, exceptions, …) and they are structured in different ways (functions or classes).
How could we see that all these codes are validations? Here some tips:
The function or class name contains terms like
verification
orvalidate
The return type is
boolean
orValidated
There is exceptions returned
The code structure is a simple condition or a simple
if/else
statement with a return value for each branch
🤔 Seems like there is a code pattern. Could we introduce a language feature?
Verification syntax
Literal message
Definiti defines a syntax for verification functions:
verification NonEmptyString {
"The string should not be empty"
(value: String) => {
string.nonEmpty()
}
}
That's all. We use the same pattern that other languages but give it a specific syntax. When compiling into other languages, it will be the role of the compiler to use the specific technical structure in your language.
Here, we use a hard-coded message but you can use a code (eg: error.non.empty.string
) for your translation system too.
Typed message
Literal messages are good for generic messages but how can you add information in your messages? Definiti provides a syntax for that:
verification PositiveNumber {
// Declare a message with the code "positive.number" and a parameter of type Number
// For instance, the message could be "The number '{0}' is negative"
message("positive.number", Number)
(value: Number) => {
if (value >= 0) {
ok
} else {
ko(value)
}
}
}
Now, your message can have more information. You can give has many parameters as you want. The ok
instruction does not accept any parameter (the value is valid, that's all) but ko
instruction requires as many parameters as the message (except the message code) and in the declared types.
You can see that the first parameter is a message code and not the message itself. To enforce the usage of a internationalization system, Definiti does no string interpolation for verifications.
Message parameter types can be any type, native or project types. The responsibility of converting them into strings is not managed by Definiti. Also, it ensures you to give the right type and right number of types.
Code parameters
Some verifications can also accept parameters to be more generic. For instance, to declare a verification checking that a number is between a specific range, you can do:
verification IsBetween(min: Number, max: Number) {
// Message sample: The value should be between {min} and {max}.
message("is.between", Number, Number)
(value: Number) => {
if (min <= value && value <= max) {
ok
} else {
ko(min, max)
}
}
}
The parameters min
and max
are defined for the verification and can be used inside the function. You can use them in the condition as well as the ko
instruction. It is just parameters like any other.
Generics
If you need to declare a generic in your verification, you can use following syntax:
verification NonEmptyList {
"The list should not be empty"
[A](value: List[A]) => {
value.nonEmpty()
}
}
Usage
You can use the verification as follow:
// Some verifications declared anywhere
verification IsNonEmptyString {
"Please give a string"
(value: String) => {…}
}
verification MaxLengthOf200 {
"You cannot give more than 200 characters"
(value: String) => {…}
}
verification IsBetween(min: Number, max: Number) {
message("is.between", Number, Number)
(value: Number) => {…}
}
// Project types are also accepted!
verification IsPersonComplete {
"The person should be complete"
(value: Person) => {…}
}
// Defined type
// For a Person to be valid, it also need to respect verification IsPersonComplete
type Person verifying IsPersonComplete {
// When empty, gives the message "Please give a string"
// When length > 200, gives the message "You cannot give more than 200 characters"
firstName: String verifying IsNonEmptyString verifying MaxLengthOf200
// When erroneous, gives the message "Please give a last name" because explicited here
lastName: String verifying IsNonEmptyString("Please give a last name")
// When erroneous, gives the message message("is.between", 18, 150)
age: Number verifying IsBetween(18, 150)
// When erroneous, gives the message "Hmm… Are you sure?" (with no parameter)
numberOfSibling: Number verifying IsBetween(1, 20, "Hmm… Are you sure?")
}
// Alias type
type NonEmptyString = String verifying IsNonEmptyString
Please refer to other chapters for more information of each syntax.
Last updated