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, ...)
- 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). - Nilai validasi akhir bukanlah objek yang kompleks, tetapi hanya benar / salah, yang akhirnya kami khawatirkan. Ini bukan keuntungan, tetapi penyederhanaan.
- Dalam Dry, hash eksternal didefinisikan sedikit berbeda dari internal. Di tingkat eksternal, metode
Schema.Params
, dan di dalam #hash
. - (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 transformasiStValidation.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')
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 :)