DSL آخر للتحقق من الصحة

لقد كتبت مؤخرًا جوهرة صغيرة لعمليات التحقق من الصحة وأرغب في مشاركتها معك في تنفيذها.


الأفكار التي تم متابعتها عند إنشاء المكتبة:


  • سهولة
  • نقص السحر
  • سهل التعلم
  • إمكانية التخصيص والحد الأدنى من القيود.

ترتبط كل هذه النقاط تقريبًا بالبساطة الأولى. التنفيذ النهائي صغير بشكل لا يصدق ، لذلك لن أستغرق الكثير من وقتك.


شفرة المصدر يمكن العثور عليها هنا .


هندسة معمارية


بدلاً من استخدام 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, ...) 

  1. يتم استخدام التجزئة لوصف التجزئة. تُستخدم القيم لوصف القيم (الفئات ، المصفوفات ، المجموعات ، الوظائف المجهولة). لا توجد طرق سحرية (لا #build اعتبار #build ، لأنها مجرد اختصار).
  2. قيمة التحقق من الصحة النهائية ليست كائنًا معقدًا ، ولكن ببساطة صواب / خطأ ، الأمر الذي نشعر بالقلق في النهاية. هذه ليست ميزة ، ولكن التبسيط.
  3. في الجاف ، يتم تعريف التجزئة الخارجية بشكل مختلف قليلاً عن الداخلية. على المستوى الخارجي ، يتم Schema.Params طريقة Schema.Params ، وداخل #hash .
  4. (المكافأة) في حالتي ، لا يجب أن يكون الكائن الذي تم التحقق من صحته علامة تجزئة ، ولا يلزم بناء جملة خاص: 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') # ==> false is_int_pair = validator_factory.build([:int, :int]) is_int_pair.call([1, 2]) # ==> true is_int_pair.call([1, '2']) # ==> false 

آسف للرمز مربكة بعض الشيء. في جوهرها ، يتحقق الصفيف في هذه الحالة من الامتثال حسب الفهرس.


يؤدي


لكن ليس هو. أنا فخور بهذا الحل التقني وأردت إثبات ذلك :)

Source: https://habr.com/ru/post/ar479600/


All Articles