لقد كتبت مؤخرًا جوهرة صغيرة لعمليات التحقق من الصحة وأرغب في مشاركتها معك في تنفيذها.
الأفكار التي تم متابعتها عند إنشاء المكتبة:
- سهولة
- نقص السحر
- سهل التعلم
- إمكانية التخصيص والحد الأدنى من القيود.
ترتبط كل هذه النقاط تقريبًا بالبساطة الأولى. التنفيذ النهائي صغير بشكل لا يصدق ، لذلك لن أستغرق الكثير من وقتك.
شفرة المصدر يمكن العثور عليها هنا .
هندسة معمارية
بدلاً من استخدام DSL المعتاد باستخدام أساليب الفصل والكتلة ، قررت استخدام البيانات.
وبالتالي ، بدلاً من الإلزامية التصريحية المعتادة (haha ، حسناً ، أنت تفهم ، نعم؟ "التعريف الإلزامي") DSL ، على سبيل المثال ، في Dry ، يحول DSL الخاص بي فقط بعض البيانات إلى مصادقة. هذا يعني أيضًا أنه يمكن تنفيذ هذه المكتبة (نظريًا) بلغات ديناميكية أخرى (على سبيل المثال ، python) ، وليس بالضرورة نحو الكائنات.
قرأت الفقرة الأخيرة وأدركت أنني كتبت نوعًا من الفوضى. انا اسف أولاً ، سأقدم بعض التعريفات ثم أعطي مثالاً على ذلك.
حدد
بنيت المكتبة بأكملها على ثلاثة مفاهيم بسيطة: المدقق ، المخطط ، والتحول .
- المدقق هو ماهية المكتبة. كائن يتحقق لمعرفة ما إذا كان هناك شيء يفي بمتطلباتنا.
- المخطط هو عبارة عن بيانات عشوائية تصف البيانات الأخرى (الغرض من التحقق من الصحة).
- التحويل هو دالة
t(b, f)
تأخذ الدائرة والكائن الذي يستدعي هذه الوظيفة (factory) ، ويعيد إما دائرة أخرى أو مدقق.
بالمناسبة ، كلمة "تحويل" في السياق في الرياضيات هي مرادف لكلمة "وظيفة" (في أي حال ، في الكتاب الذي قرأته في الجامعة).
المصنع ، رسميا ، يقوم بما يلي:
- بالنسبة لمجموعة من التحويلات
T1, T2, ..., Tn
، يتم تكوين تكوين Ta(Tb(Tc(...)))
(الترتيب تعسفي). - يتم تطبيق التكوين الناتج على الدائرة بشكل دوري حتى تختلف النتيجة عن الوسيطة.
هذا يذكرني آلة تورينج. في الإخراج ، يجب أن نحصل على أداة التحقق (أو وظيفة مجهولة). أي شيء آخر يعني أن المخطط و / أو التحويلات غير صحيحة.
مثال
على رديت ، أعطى رجل مثال في الجاف:
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?
كما ترون ، يتم استخدام السحر في شكل required(..).value
وطرق مثل #array
.
قارن مع المثال الخاص بي:
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, ...)
- يتم استخدام التجزئة لوصف التجزئة. تُستخدم القيم لوصف القيم (الفئات ، المصفوفات ، المجموعات ، الوظائف المجهولة). لا توجد طرق سحرية (لا
#build
اعتبار #build
، لأنها مجرد اختصار). - قيمة التحقق من الصحة النهائية ليست كائنًا معقدًا ، ولكن ببساطة صواب / خطأ ، الأمر الذي نشعر بالقلق في النهاية. هذه ليست ميزة ، ولكن التبسيط.
- في الجاف ، يتم تعريف التجزئة الخارجية بشكل مختلف قليلاً عن الداخلية. على المستوى الخارجي ، يتم
Schema.Params
طريقة Schema.Params
، وداخل #hash
. - (المكافأة) في حالتي ، لا يجب أن يكون الكائن الذي تم التحقق من صحته علامة تجزئة ، ولا يلزم بناء جملة خاص:
is_int = StValidation.build(Integer)
.
كل عنصر من عناصر الدائرة نفسها هو دائرة. التجزئة هي مثال على مخطط معقد (أي ، مخطط يتكون من مخططات أخرى).
هيكل
تتكون الجوهرة بأكملها من عدد صغير من الأجزاء:
- مساحة الاسم الرئيسية (الوحدة)
StValidation
- المصنع المسؤول عن توليد أجهزة
StValidation::ValidatorFactory
هو StValidation::ValidatorFactory
. - المدقق التجريدي
StValidation::AbstractValidator
، والذي ، في الواقع ، واجهة. - مجموعة
StValidation::Validators
من الصحة الأساسية التي قمت بتضمينها في "بناء الجملة" الأساسي في الوحدة النمطية StValidation::Validators
- طريقتان للوحدة الرئيسية للراحة والجمع بين جميع العناصر الأخرى:
StValidation.build
- باستخدام مجموعة قياسية من التحويلاتStValidation.with_extra_transformations
- باستخدام مجموعة قياسية من التحويلات ، ولكن مع توسيعها.
معيار DSL
قمت بتضمين العناصر التالية في خط المشترك الرقمي الخاص بي:
- فئة - يتحقق من نوع الكائن (على سبيل المثال ، عدد صحيح).
أبسط مدقق في بناء الجملة الخاص بي ، بصرف النظر عن الوظيفة المجهولة وأحفاد AbstractValidator ، والتي هي أوليات المولد. - والجمهور هو اتحاد المخططات. مثال:
Set[Integer, ->(x) { x.nil? }]
Set[Integer, ->(x) { x.nil? }]
.
يتحقق من تطابق الكائن مع أحد المخططات على الأقل. حتى الفئة نفسها تسمى UnionValidator
.
أبسط مثال هو مدقق مركب. - الصفيف هو مثال:
[Integer]
.
يتحقق من أن الكائن عبارة عن صفيف وأن جميع عناصره تلبي نظامًا معينًا. - التجزئة هي نفسها ، ولكن بالنسبة للتجزئة. غير مسموح باستخدام مفاتيح اضافية.
تبدو مجموعة التحولات كما يلي:
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
ليس هناك مكان أسهل ، أليس كذلك؟
أخطاء و # تفسير
بالنسبة لي شخصيا ، فإن الغرض الرئيسي من عمليات التحقق هو التحقق من صحة الكائن. لماذا هو غير صالح هو سؤال جانبي.
ومع ذلك ، من المفيد أن نفهم سبب عدم صلاحية شيء ما. للقيام بذلك ، أضفت طريقة #explain
إلى واجهة المدقق.
بشكل أساسي ، يجب أن تفعل نفس الشيء مثل التحقق من الصحة ، ولكن إرجاع ما هو خطأ على وجه التحديد.
بشكل عام ، يمكن تعريف التحقق من الصحة نفسه ( #call
) كحالة خاصة لـ #explain
، فقط عن طريق التحقق مما إذا كانت نتيجة التوضيح فارغة.
مثل هذا التحقق ، سيكون أبطأ (لكن هذا ليس مهمًا).
لأن دالات مجهولة المصدر التفاف في نفسها في سليل AbstractValidator
، كما أن لديها طريقة #explain
وتشير ببساطة إلى حيث يتم تعريف الوظيفة.
عند كتابة #explain
، يمكن أن يكون #explain
بشكل تعسفي.
التخصيص
لم يتم بناء "بناء الجملة" الخاص بي في قلب المكتبة ، وبالتالي ، ليس مطلوبًا. (انظر StValidation.build
).
دعنا نجرب DSL أبسط يتضمن فقط الأرقام والسلاسل والمصفوفات:
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')
آسف للرمز مربكة بعض الشيء. في جوهرها ، يتحقق الصفيف في هذه الحالة من الامتثال حسب الفهرس.
يؤدي
لكن ليس هو. أنا فخور بهذا الحل التقني وأردت إثبات ذلك :)