2015-05-12 13 views
5

Angenommen, ich definiere eine Metaklasse, die Standard-Slots mit einem Validator-Slot erweitert, wenn ich :validator (clavier:valid-email "The email is invalid") als Option übergebe, anstatt das Ergebnis des Ausdrucks zu speichern ein Funcallable, speichert es den Ausdruck selbst. Fehle ich bei der Erweiterung der Standard-Slots einen Schritt? Wie stelle ich sicher, dass der Ausdruck vor dem Speichern ausgewertet wird? Ich benutze SBCL 1.2.11 btw. Hier ist der Code in FrageBenutzerdefinierte Slot-Optionen wenden keine Reduktion auf ihr Argument an

(unless (find-package 'clavier) 
    (ql:quickload :clavier)) 
(unless (find-package 'c2mop) 
    (ql:quickload :c2mop)) 
(defpackage #:clos2web/validation 
    (:use #:cl) 
    (:import-from #:c2mop 
       #:standard-class 
       #:standard-direct-slot-definition 
       #:standard-effective-slot-definition 
       #:validate-superclass 
       #:direct-slot-definition-class 
       #:effective-slot-definition-class 
       #:compute-effective-slot-definition 
       #:slot-value-using-class)) 

(in-package #:clos2web/validation) 

(defun true (value) 
    "Always return true." 
    (declare (ignore value)) 
    t) 

(defclass validation-class (standard-class) 
() 
    (:documentation "Meta-class for objects whose slots know how to validate 
    their values.")) 

(defmethod validate-superclass 
    ((class validation-class) (super standard-class)) 
    t) 

(defmethod validate-superclass 
    ((class standard-class) (super validation-class)) 
    t) 

(defclass validation-slot (c2mop:standard-slot-definition) 
    ((validator :initarg :validator :accessor validator :initform #'true 
       :documentation "The function to determine if the value is 
    valid. It takes as a parameter the value."))) 

(defclass validation-direct-slot (validation-slot 
            standard-direct-slot-definition) 
()) 

(defclass validation-effective-slot (validation-slot 
            standard-effective-slot-definition) 
()) 

(defmethod direct-slot-definition-class ((class validation-class) &rest initargs) 
    (declare (ignore initargs)) 
    (find-class 'validation-direct-slot)) 

(defmethod effective-slot-definition-class ((class validation-class) &rest initargs) 
    (declare (ignore initargs)) 
    (find-class 'validation-effective-slot)) 

(defmethod compute-effective-slot-definition 
    ((class validation-class) slot-name direct-slot-definitions) 
    (let ((effective-slot-definition (call-next-method))) 
    (setf (validator effective-slot-definition) 
      (some #'validator direct-slot-definitions)) 
    effective-slot-definition)) 

(defmethod (setf slot-value-using-class) :before 
    (new (class validation-class) object (slot validation-effective-slot)) 
    (when (slot-boundp slot 'validator) 
    (multiple-value-bind (validp msg) 
     (funcall (validator slot) new) 
     (unless validp 
     (error msg))))) 

;; Example usage 

(defclass user() 
    ((name :initarg :name) 
    (email :initarg :email :validator (clavier:valid-email "The email is invalid") :accessor email)) 
    (:metaclass validation-class)) 

(let ((pepe (make-instance 'user :name "Pepe" :email "[email protected]"))) 
    (setf (email pepe) "FU!")) ;; should throw 

Der Code schlägt fehl, wenn eine Instanz als (CLAVIER: VALID-EMAIL "Die E-Mail ist ungültig") zu machen ist kein funcallable.

+0

'Defclass' wertet Dinge nicht aus, daher wird das Formular als Slot-Option gespeichert. Ich denke über die beste Lösung nach. – Svante

+0

in sbcl die canonicalize-defclass-slots kümmert sich um die Verarbeitung der Slots und hat Zugriff auf die env der Definition, haben Sie irgendwelche Hinweise, wie es möglich, nicht-Standard-Optionen mit der Umgebung zu reduzieren? – PuercoPop

Antwort

5

Wie der Kommentar oben sagt, defclass wertet Argumente nicht aus (es ist ein Makro). Während der übliche Rat ist, eval zu vermeiden, denke ich, dass eval in diesem Umstand genau sein könnte, was Sie wollen. Während Sie normalerweise das Formular direkt in einen Makrokörper spleißen, mit defclass denke ich, ist die Antwort, das Formular in der Schlitzinitialisierung auszuwerten und die Auswertung zu speichern (wenn es noch nicht ausgewertet worden ist).

Dies würde wahrscheinlich auftreten in:

(defmethod initialize-instance :after ((obj validation-slot) 
             &key &allow-other-keys) 
    #| ... |#) 

Optional können Sie auch die :validation-message und :validation-fn als zwei getrennte Argumente speichern dann rufen:

(multiple-value-bind (validp msg) 
    (funcall (funcall (validator-fn slot) 
         (validator-message slot)) 
      new) 
    (unless validp 
    (error msg))) 

Eine weitere Alternative, um die Bewertung der speichern wäre Formular und übergeben Sie das an das Makro:

(defvar *email-validator* (CLAVIER:VALID-EMAIL "The email is invalid")) 
(defun email-validator (val) 
    (funcall *email-validator* val)) 

Dann passiere email-validator nach defclass.

Zusätzlich könnte ich vorschlagen, dass Ihre Validierungsfunktionen slot-validation-error Typ Bedingungen anstelle von error Typ Bedingungen signalisieren. Dann könnte Ihre Bedingung Verweise auf den fehlgeschlagenen Validierer, den Wert, den Slot und die Instanz enthalten. Dies könnte Ihnen eine viel bessere Kontrolle als der rohe Fehler geben. Sie könnten auch einige Neustarts hinzufügen (Abbrechen, um das Setzen des Steckplatzes zu überspringen, Use-Value, um einen anderen Wert anzugeben).

Abhängig von Ihrer Konfiguration kann es auch sinnvoller sein, dass Ihre Validierungsfunktion diese direkt signalisiert, anstatt mehrere Werte zurückzugeben, die dann zu Signalen gezwungen werden.

+0

Danke für den Zeiger auf Schlitzvalidierungsfehler. Nun, ich denke, es ist eval für jetzt. Ich sollte wahrscheinlich schauen, wie initform die Umgebung zum Zeitpunkt der Definition verfolgt, um eine andere Alternative zu sehen. – PuercoPop