Validation System for mito ORM.
The purpose of this sytem is to make it easier to integrate validation with mito by ensuring validation happens before an insert to the DB and by providing a convenient place to define the validations.
Right now this makes it very easy to add validation based on types and functions to slots, and with a function to the whole object.
TODO: We are planning on adding inferred type validation as well as a macro for ease of defining the class level settings.
Please note GitHub is not rendering TODO items in this org file. The inferred and macro sections are not finished.
NOTE: This is not a data validation library. Rather this is a convenient way to integrate validation with mito objects, and ensure they are validated before a DB call is made. For data validation libraries, take a look at the cliki and the awesome-cl list of projects. Data validation libraries are meant to be used together with this system for ease of use. They are not exclusive of each other.
Right now the source code is hosted here: https://github.com/daninus14/mito-validate
Please check in the future for quicklisp and osicl availability.
Note that because of an implementation error in mito signaling an error unnecessarily, computing effective slots is not possible, so until they merge our PR, please use our up to date (as of 2024/10/31) repository for mito here https://github.com/daninus14/mito
First, load the system into the lisp image.
(ql:quickload "mito-validate")
Then simply add the mito-validate-metaclass
metaclass to the class definition of the DB table.
The keys :validation-type
and :validation-function
can be used at each slot definition, and the key :validation-function
can be used at the class level definition to provide slot level and object level validation respectively.
Here’s a simple example:
(defclass customer ()
;; only people 18 and older can make purchases
((age
:type :integer
:col-type (or :null :integer)
:validation-type (integer 18 ))
;; function below checkes input has only alphabetical characters and is not empty
(name
:type :text
:col-type (or :null :text)
:validation-function
(lambda (x)
(unless
(cl-ppcre:all-matches-as-strings "^[a-zA-Z ]+$"
x)
(error "name can only have letters and space!"))))
; this will automatically check in CL that data is an integer
(favorite-number
:type :integer
:col-type (or :null :integer)))
(:metaclass mito-validate:mito-validate-metaclass))
=> #<MITO-VALIDATE-METACLASS MITO-VALIDATE::CUSTOMER>
MITO-VALIDATE> (mito:insert-dao (make-instance 'customer :age 16 :name "Phil" :favorite-number 24))
;; Debugger entered on #<TYPE-ERROR expected-type: (INTEGER 18) datum: 16>
[1] MITO-VALIDATE>
;; Evaluation aborted on #<TYPE-ERROR expected-type: (INTEGER 18) datum: 16>
MITO-VALIDATE> (mito:insert-dao (make-instance 'customer :age 20 :name "Phil18" :favorite-number 24))
;; Debugger entered on #<SIMPLE-ERROR "name can only have letters and space!" {10050BD403}>
[1] MITO-VALIDATE>
;; Evaluation aborted on #<SIMPLE-ERROR "name can only have letters and space!" {10050BD403}>
MITO-VALIDATE> (mito:insert-dao (make-instance 'customer :age 20 :name "Phil" :favorite-number "24"))
#<CUSTOMER {1004F654F3}>
This will add automatic Common Lisp validation before sending the request to the DataBase. See the reference below for more details.
See the example below for an object level validation example, and the manual for more details.
Here’s an example with the deftablev
macro, which is basically a defclass
under the hood with the addition of adding the object level validation.
(deftablev c4 ()
((items
:accessor items
:col-type (or :null :integer))
(price
:accessor price
:col-type (or :null :integer)))
(:validation-function (lambda (x)
(when (< 10 (* (price x)
(items x)))
(error "Purchase total cannot exceed 10!"))))
(:documentation "This is a sample table"))
The above is the equivalent of:
(defclass c4 ()
((items
:accessor items
:col-type (or :null :integer))
(price
:accessor price
:col-type (or :null :integer))))
(setf (validation-function (find-class c4))
(lambda (x)
(when (< 10 (* (price x)
(items x)))
(error "Purchase total cannot exceed 10!")))))
The following macro makes it easier to define all the slot level and class level validations, as well as defining a mito table.
(defmacro deftablev (class-name superclasses slot-definitions class-validations &rest options))
Note that options must be after the validation functions.
In addition, the following macro should be helpful for defining object level validations outside of the class definition:
(defmacro set-validation (validation-key validation-value class-name))
The way mito-validate
works is by providing two types of validation:
- Slot level validation
- Object level validation
Any validation can be skipped by adding the appropriate keyword to the metaclass or slot definition.
Validations will be triggered when (mito:insert-dao)
or (mito:save-dao)
are called.
NOTE: THIS IS NOT YET IMPLEMENTED mito-validate will try to make a validation type based on the provided mito type of the slot. Please note that the validation will be based on CL types.
This functionality is disabled by default.
To infer the validation type on a slot, add to the slot definition :infer-validation T
.
To infer the validation type for all the slots on a class, whenever there is no other validation in that slot, apply :infer-validation T
to the class itself.
Here is a list of the mito SQL types and the Common Lisp types that will be used to validate them
list here mito types, and what CL types I'm using to validate the data.
A type can be provided to any slot with the key :validation-type
in the slot definition.
mito-validate will signal an error unless the type of the data fits the provided type as follows:
(error
'type-error
:expected-type (validation-type-slot-value slot)
:datum (slot-value obj
(closer-mop:slot-definition-name slot)))
A validation function can be provided to any slot with the key :validation-function
in the slot definition.
mito-validate will simply evaluate the function passing it the slot data as the sole argument.
The function must therefore fit the following function:
(lambda (x))
The function should signal an error condition in case the data is invalid; otherwise the data will be assumed to be valid.
Any returned values are ignored.
A validation function which will receive the object as its input can be provided in the class definition with the key :validation-function
in the metaclass.
The function takes in only one argument, which is the object itself.
(lambda (x))
The function should signal a condition in case the data is invalid; otherwise the data will be assumed to be valid.
Any returned values are ignored.
Here’s an example:
(defclass purchase ()
((items
:accessor items
:col-type (or :null :integer))
(price
:accessor price
:col-type (or :null :integer)))
(:metaclass mito-validate-metaclass))
MITO-VALIDATE> (mito:insert-dao (make-instance 'purchase :items 3 :price 4))
#<PURCHASE {100422EAD3}>
MITO-VALIDATE> (price *)
4 (3 bits, #x4, #o4, #b100)
MITO-VALIDATE> (setf (validation-function (find-class 'purchase))
(lambda (x)
(when (< 10 (* (price x)
(items x)))
(error "Purchase total cannot exceed 10!"))))
#<FUNCTION (LAMBDA (X)) {B8011D273B}>
MITO-VALIDATE> (mito:insert-dao (make-instance 'purchase :items 3 :price 4))
; Debugger entered on #<SIMPLE-ERROR "Purchase total cannot exceed 10!" {1006ECDB93}>
[1] MITO-VALIDATE>
; Evaluation aborted on #<SIMPLE-ERROR "Purchase total cannot exceed 10!" {1006ECDB93}>
By providing the key :skip-validation
in the slot definition, the slot level validation will be skipped.
This will skip all validations, whether they be DB Derived Validations, or provided type or function validations.
By providing :skip-validation
in the class definition, all validations will be skipped even if explicitly declared.
Here’s an example of skipping all class level validations:
MITO-VALIDATE> (skip-validation (find-class 'c2))
NIL
MITO-VALIDATE> (setf (skip-validation (find-class 'c2)) T)
T
MITO-VALIDATE> (mito:insert-dao (make-instance 'c2 :name "ron" :email "[email protected]" :age-claimed 17))
#<C2 {100410B213}>
MITO-VALIDATE> (setf (skip-validation (find-class 'c2)) NIL)
NIL
MITO-VALIDATE> (mito:insert-dao (make-instance 'c2 :name "ron" :email "[email protected]" :age-claimed 17))
; Debugger entered on #<TYPE-ERROR expected-type: (INTEGER 18) datum: 17> ; ; ; ; ; ; ; ; ;
[1] MITO-VALIDATE>
; Evaluation aborted on #<TYPE-ERROR expected-type: (INTEGER 18) datum: 17> ; ; ; ; ; ; ; ; ;
By providing the key :skip-slot-validations
in the class definition, all the slot level validations will be skipped.
See above “Skip All Validation” for an example of setting the class level properties.
By providing the key :skip-object-validation
in the class definition, the object level validation will be skipped.
See above “Skip All Validation” for an example of setting the class level properties.