DSL lain untuk validasi

Baru-baru ini saya menulis permata kecil untuk validasi dan ingin berbagi dengan Anda implementasinya.


Gagasan yang diupayakan saat membuat perpustakaan:


  • Kesederhanaan
  • Kurangnya sihir
  • Mudah dipelajari
  • Kemungkinan penyesuaian dan batasan minimum.

Hampir semua poin ini terkait dengan yang pertama - kesederhanaan. Implementasi terakhir sangat kecil, jadi saya tidak akan menghabiskan banyak waktu Anda.


Kode sumber dapat ditemukan di sini .


Arsitektur


Alih-alih menggunakan DSL yang biasa menggunakan metode kelas dan blok, saya memutuskan bahwa saya akan menggunakan data.
Jadi, bukannya imperatif deklaratif biasa (haha, yah, Anda mengerti, ya? "Deklaratif-imperatif") DSL seperti, misalnya, di Dry, DSL saya hanya mengubah beberapa data yang ditetapkan menjadi validator. Ini juga berarti bahwa perpustakaan ini dapat diimplementasikan (secara teoritis) dalam bahasa dinamis lain (misalnya, python), bahkan tidak harus berorientasi objek.


Saya membaca paragraf terakhir dan mengerti bahwa saya menulis semacam kekacauan. Maafkan aku Pertama, saya akan memberikan beberapa definisi dan kemudian memberi contoh.


Definisi


Seluruh perpustakaan dibangun di atas tiga konsep sederhana: validator , cetak biru, dan transformasi .


  • Validator adalah untuk apa perpustakaan itu. Objek yang memeriksa untuk melihat apakah sesuatu memenuhi persyaratan kami.
  • Skema hanyalah data acak yang menggambarkan data lain (tujuan validasi kami).
  • Transformasi adalah fungsi t(b, f) yang mengambil sirkuit dan objek yang memanggil fungsi ini (pabrik), dan mengembalikan sirkuit lain atau validator.
    Omong-omong, kata "konversi" secara kontekstual dalam matematika identik dengan kata "fungsi" (dalam kasus apa pun, dalam buku yang saya baca di universitas).

Pabrik, secara resmi, melakukan hal berikut:


  • Untuk seperangkat transformasi T1, T2, ..., Tn , komposisi Ta(Tb(Tc(...))) (urutannya berubah-ubah).
  • Komposisi yang dihasilkan diterapkan pada rangkaian secara siklis hingga hasilnya berbeda dari argumen.

Ini mengingatkan saya pada mesin Turing. Pada output, kita harus mendapatkan validator (atau fungsi anonim). Hal lain berarti skema dan / atau transformasi tidak benar.


Contoh


Pada reddit, seorang pria memberi contoh di Dry:


 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? 

Seperti yang Anda lihat, sihir digunakan dalam bentuk yang required(..).value dan metode seperti #array .


Bandingkan dengan contoh saya:


 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. Hash digunakan untuk menggambarkan hash. Nilai digunakan untuk menggambarkan nilai (kelas, array, set, fungsi anonim). Tidak ada metode ajaib ( #build tidak dipertimbangkan, karena itu hanya singkatan).
  2. Nilai validasi akhir bukanlah objek yang kompleks, tetapi hanya benar / salah, yang akhirnya kami khawatirkan. Ini bukan keuntungan, tetapi penyederhanaan.
  3. Dalam Dry, hash eksternal didefinisikan sedikit berbeda dari internal. Di tingkat eksternal, metode Schema.Params , dan di dalam #hash .
  4. (bonus) dalam kasus saya, objek yang divalidasi tidak harus hash, dan tidak memerlukan sintaks khusus: is_int = StValidation.build(Integer) .
    Setiap elemen dari sirkuit itu sendiri adalah sirkuit. Hash adalah contoh skema kompleks (mis., Skema yang terdiri dari skema lain).

Struktur


Seluruh permata terdiri dari sejumlah kecil bagian:


  • Namespace (modul) utama StValidation
  • Pabrik yang bertanggung jawab untuk menghasilkan validator adalah StValidation::ValidatorFactory .
  • Abstract validator StValidation::AbstractValidator , yang sebenarnya adalah sebuah antarmuka.
  • Himpunan validator dasar yang saya sertakan dalam "sintaksis" dasar dalam modul StValidation::Validators
  • Dua metode modul utama untuk kenyamanan dan menggabungkan semua elemen lainnya:
    • StValidation.build - menggunakan seperangkat standar transformasi
    • StValidation.with_extra_transformations - menggunakan seperangkat standar transformasi, tetapi memperluasnya.

DSL standar


Saya menyertakan elemen-elemen berikut dalam DSL saya sendiri:


  • Kelas - memeriksa jenis objek (misalnya, Integer).
    Validator paling sederhana di sintaks saya, terlepas dari fungsi anonim dan keturunan AbstractValidator, yang merupakan primitif dari generator.
  • Orang banyak adalah penyatuan skema. Contoh: Set[Integer, ->(x) { x.nil? }] Set[Integer, ->(x) { x.nil? }] .
    Periksa apakah objek cocok dengan setidaknya satu skema . Bahkan kelas itu sendiri disebut UnionValidator .
    Contoh paling sederhana adalah validator komposit.
  • Array adalah contoh: [Integer] .
    Periksa bahwa objek adalah array dan semua elemen memenuhi skema tertentu.
  • Hash adalah sama, tetapi untuk hash. Kunci tambahan tidak diizinkan.

Himpunan transformasi terlihat seperti ini:


 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 # ... 

Tidak ada tempat yang lebih mudah, bukan?


Kesalahan dan # jelaskan


Bagi saya pribadi, tujuan utama validasi adalah untuk memeriksa apakah suatu objek valid. Mengapa tidak valid adalah pertanyaan sampingan.
Namun, penting untuk memahami mengapa sesuatu tidak valid. Untuk melakukan ini, saya menambahkan metode #explain ke antarmuka validator.


Pada dasarnya, ia harus melakukan hal yang sama dengan validasi, tetapi mengembalikan apa yang secara spesifik salah.
Secara umum, validasi itu sendiri ( #call ) dapat didefinisikan sebagai kasus khusus #explain , hanya dengan memeriksa jika hasil yang dijelaskan kosong.


Namun validasi seperti itu akan lebih lambat (tetapi ini tidak penting).


Karena Fungsi predikat anonim membungkus diri dalam keturunan AbstractValidator , mereka juga memiliki metode #explain dan hanya menunjukkan di mana fungsi didefinisikan.


Saat menulis validator khusus, #explain dapat menjadi rumit dan cerdas.


Kustomisasi


"Sintaks" saya tidak ada di jantung perpustakaan dan, oleh karena itu, tidak diperlukan. (lihat StValidation.build ).


Mari kita coba DSL yang lebih sederhana yang hanya akan memasukkan angka, string, dan array:


 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 

Maaf untuk kode yang sedikit membingungkan. Intinya, array dalam hal ini memeriksa kepatuhan dengan indeks.


Ringkasan


Tapi bukan dia. Saya hanya bangga dengan solusi teknis ini dan ingin menunjukkannya :)

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


All Articles