Ich habe kürzlich ein kleines Juwel für Validierungen geschrieben und möchte Ihnen die Implementierung mitteilen.
Ideen, die bei der Erstellung der Bibliothek verfolgt wurden:
- Einfachheit
- Mangel an Magie
- Leicht zu erlernen
- Die Möglichkeit der Anpassung und ein Minimum an Einschränkungen.
Fast alle diese Punkte hängen mit der Einfachheit zusammen. Die endgültige Implementierung ist unglaublich klein, daher nehme ich mir nicht viel Zeit für Sie.
Den Quellcode finden Sie hier .
Architektur
Anstatt das übliche DSL mit Klassen- und Blockmethoden zu verwenden, habe ich beschlossen, die Daten zu verwenden.
Anstelle des üblichen deklarativen Imperativs (haha, verstehen Sie, ja? "Deklarativen Imperativ") wandelt DSL, wie zum Beispiel in Dry, einfach einige Daten in einen Validator um. Dies bedeutet auch, dass diese Bibliothek (theoretisch) in anderen dynamischen Sprachen (z. B. Python) implementiert werden kann, nicht unbedingt sogar objektorientiert.
Ich habe den letzten Absatz gelesen und verstanden, dass ich ein Durcheinander geschrieben habe. Ich bitte um Entschuldigung. Zuerst werde ich einige Definitionen geben und dann ein Beispiel geben.
Definitionen
Die gesamte Bibliothek basiert auf drei einfachen Konzepten: Validator , Blueprint und Transformation .
- Der Validator ist, wofür die Bibliothek ist. Ein Objekt, das prüft, ob etwas unseren Anforderungen entspricht.
- Ein Schema besteht einfach aus beliebigen Daten, die andere Daten beschreiben (der Zweck unserer Validierung).
- Eine Transformation ist eine Funktion
t(b, f)
, die eine Schaltung und das Objekt, das diese Funktion aufruft (Factory), aufnimmt und entweder eine andere Schaltung oder einen Validator zurückgibt.
Übrigens ist das Wort "Umwandlung" in der Mathematik ein Synonym für das Wort "Funktion" (jedenfalls in dem Buch, das ich an der Universität gelesen habe).
Die Fabrik macht formal Folgendes:
- Für eine Menge von Transformationen
T1, T2, ..., Tn
wird eine Zusammensetzung Ta(Tb(Tc(...)))
(die Reihenfolge ist beliebig). - Die resultierende Komposition wird zyklisch auf die Schaltung angewendet, bis das Ergebnis vom Argument abweicht.
Es erinnert mich an eine Turingmaschine. Am Ausgang sollten wir einen Validator (oder eine anonyme Funktion) bekommen. Alles andere bedeutet, dass das Schema und / oder die Transformationen falsch sind.
Beispiel
Auf reddit gab ein Mann in Dry ein Beispiel:
user_schema = Dry::Schema.Params do required(:id).value(:integer) required(:name).value(:string) required(:age).value(:integer, included_in?: 0..150) required(:favourite_food).value(array[:string]) required(:dog).maybe do hash do required(:name).value(:string) required(:age).value(:integer) optional(:breed).maybe(:string) end end end user_schema.call(id: 123, name: "John", age: 18, ...).success?
Wie Sie sehen, wird Magie in Form von required(..).value
und Methoden wie #array
.
Vergleichen Sie mit meinem Beispiel:
is_valid_user = StValidation.build( id: Integer, name: String, age: ->(x) { x.is_a?(Integer) && (0..150).cover?(x) }, favourite_food: [String], dog: Set[NilClass, { name: String, age: Integer, breed: Set[NilClass, String] }] ) is_valid_user.call(id: 123, name: 'John', age: 18, ...)
- Ein Hash wird verwendet, um einen Hash zu beschreiben. Werte werden zur Beschreibung von Werten verwendet (Klassen, Arrays, Mengen, anonyme Funktionen). Keine magischen Methoden (
#build
nicht berücksichtigt, da es sich nur um eine Abkürzung handelt). - Der endgültige Validierungswert ist kein komplexes Objekt, sondern einfach wahr / falsch, worüber wir uns letztendlich Gedanken machen. Dies ist kein Vorteil, sondern eine Vereinfachung.
- In Dry unterscheidet sich der externe Hash geringfügig vom internen. Auf der externen Ebene wird die Methode
Schema.Params
und innerhalb von #hash
. - (Bonus) In meinem Fall muss das validierte Objekt kein Hash sein und es ist keine spezielle Syntax erforderlich:
is_int = StValidation.build(Integer)
.
Jedes Element der Schaltung selbst ist eine Schaltung. Ein Hash ist ein Beispiel für ein komplexes Schema (d. H. Ein Schema, das aus anderen Schemata besteht).
Struktur
Das ganze Juwel besteht aus einer kleinen Anzahl von Teilen:
StValidation
(Modul) StValidation
- Die Factory, die für die Generierung von Validatoren verantwortlich ist, ist
StValidation::ValidatorFactory
. - Abstract Validator
StValidation::AbstractValidator
, eigentlich eine Schnittstelle. - Die Gruppe der grundlegenden Validatoren, die ich in die grundlegende "Syntax" im Modul "
StValidation::Validators
- Zwei Methoden des Hauptmoduls zur Vereinfachung und Kombination aller anderen Elemente:
StValidation.build
- Verwenden eines Standardsatzes von TransformationenStValidation.with_extra_transformations
- Verwenden einer Standardmenge von Transformationen, aber Erweitern.
Standard DSL
Ich habe die folgenden Elemente in mein eigenes DSL aufgenommen:
- Klasse - Überprüft den Typ eines Objekts (z. B. Ganzzahl).
Der einfachste Validator in meiner Syntax, abgesehen von der anonymen Funktion und den Nachkommen von AbstractValidator, den Grundelementen des Generators. - Die Menge ist die Vereinigung von Schemata. Beispiel:
Set[Integer, ->(x) { x.nil? }]
Set[Integer, ->(x) { x.nil? }]
.
Überprüft, ob das Objekt mindestens einem der Schemata entspricht . Sogar die Klasse selbst heißt UnionValidator
.
Das einfachste Beispiel ist ein zusammengesetzter Validator. - Ein Array ist ein Beispiel:
[Integer]
.
Überprüft, ob das Objekt ein Array ist und ob alle seine Elemente einem bestimmten Schema entsprechen . - Ein Hash ist dasselbe, nur für Hashes. Zusätzliche Schlüssel sind nicht erlaubt.
Die Transformationen sehen folgendermaßen aus:
def basic_transformations [ ->(bp, _factory) { bp.is_a?(Class) ? class_validator(bp) : bp }, ->(bp, factory) { bp.is_a?(Set) ? union_validator(bp, factory) : bp }, ->(bp, factory) { bp.is_a?(Hash) ? hash_validator(bp, factory) : bp }, ->(bp, factory) { bp.is_a?(Array) && bp.size == 1 ? array_validator(bp[0], factory) : bp } ] end def class_validator(klass) Validators::ClassValidator.new(klass) end def union_validator(blueprint, factory) Validators::UnionValidator.new(blueprint, factory) end
Nirgendwo ist es einfacher, oder?
Fehler und #erklären
Für mich persönlich besteht der Hauptzweck von Validierungen darin, zu überprüfen, ob ein Objekt gültig ist. Warum es nicht gültig ist, ist eine Nebenfrage.
Es ist jedoch hilfreich zu verstehen, warum etwas nicht gültig ist. Zu diesem #explain
ich der Validator-Schnittstelle die Methode #explain
hinzugefügt.
Im Wesentlichen sollte es dasselbe tun wie die Validierung, aber das zurückgeben, was speziell falsch ist.
Im Allgemeinen kann die Validierung selbst ( #call
) als Sonderfall von #explain
, indem nur überprüft wird, ob das #explain
Ergebnis leer ist.
Eine solche Validierung ist jedoch langsamer (dies ist jedoch nicht wichtig).
Weil Anonyme Prädikatfunktionen hüllen sich in den Nachkommen von AbstractValidator
, haben auch die Methode #explain
und geben einfach an, wo die Funktion definiert ist.
Beim Schreiben von benutzerdefinierten Validatoren kann #explain
beliebig komplex und intelligent sein.
Anpassung
Meine "Syntax" ist nicht in das Herz der Bibliothek integriert und daher nicht erforderlich. (Siehe StValidation.build
).
Versuchen wir es mit einer einfacheren DSL, die nur Zahlen, Zeichenfolgen und Arrays enthält:
validator_factory = StValidation::ValidatorFactory.new( [ -> (blueprint, _) { blueprint == :int ? ->(x) { x.is_a?(Integer) } : blueprint }, -> (blueprint, _) { blueprint == :str ? ->(x) { x.is_a?(String) } : blueprint }, lambda do |blueprint, factory| return blueprint unless blueprint.is_a?(Array) inner_validators = blueprint.map { |b| factory.build(b) } ->(x) { x.is_a?(Array) && inner_validators.zip(x).all? { |v, e| v.call(e) } } end ] ) is_int = validator_factory.build(:int) is_int.call('123')
Entschuldigen Sie den etwas verwirrenden Code. Im Wesentlichen überprüft das Array in diesem Fall die Einhaltung des Index.
Zusammenfassung
Aber nicht er. Ich bin nur stolz auf diese technische Lösung und wollte sie demonstrieren :)