Pengenalan Malli

Mengenal malli sebagai alternatif schema berbasis data yang berorientasi performa.

Daftar isi

Malli adalah pustaka schema, deskripsi bentuk data, untuk Clojure dan ClojureScript yang skemanya berupa data biasa dan dirancang dengan orientasi performa. Berbeda dari spec.alpha yang mendaftarkan deskripsi pada keyword global, malli menempatkan schema sebagai nilai yang dikirim sebagai argumen, sehingga deskripsi bentuk data dapat didefinisikan di dekat penggunaannya. Topik ini membahas penulisan schema, pemeriksaan dengan m/validate, diagnosis dengan m/explain, transformasi masukan dengan m/decode, serta validator terkompilasi yang menjadi inti orientasi performa malli. Seluruh contoh menggunakan data antrean loket pelayanan publik dan dijalankan pada malli 0.20.3.

Menyiapkan pustaka malli#

Malli terdistribusi sebagai artefak Maven di bawah kelompok metosin, dan dependensinya dideklarasikan pada berkas deps.edn proyek. Versi berikut dipatok pada 0.20.3 agar seluruh keluaran tulisan ini dapat direproduksi.

edn deps.edn
{:deps {metosin/malli {:mvn/version "0.20.3"}}}

Simpan berkas tersebut di direktori proyek, lalu jalankan contoh dari direktori yang sama sehingga Clojure CLI menemukan dependensinya. Struktur schema malli tersusun atas vector dan keyword, misalnya [:map ...] untuk map dan [:enum ...] untuk pilihan nilai, sehingga schema dapat ditulis sebagai literal data tanpa fungsi pendaftaran apa pun. Karena bentuknya data, schema juga dapat dibangun oleh program, dikirim antar-modul, dan disimpan dalam format EDN.

Schema sebagai data dan m/validate#

Schema antrean loket berikut mendeskripsikan map dengan tiga kunci bertipe tetap dan satu pilihan layanan. Tipe ditulis sebagai predikat seperti string? dan int?, sedangkan pilihan layanan ditulis [:enum "ktp" "kk" "akta"] yang hanya menerima salah satu nilainya. Fungsi m/validate menerima schema dan data, lalu mengembalikan true atau false.

clojure Contoh: schema antrean loket dan m/validate
(require '[malli.core :as m])

(def Antrean
  [:map
   [:tiket string?]
   [:nomor int?]
   [:layanan [:enum "ktp" "kk" "akta"]]
   [:prioritas boolean?]])

(println "data lengkap    :" (m/validate Antrean {:tiket "A-012" :nomor 12 :layanan "ktp" :prioritas true}))
(println "tipe salah      :" (m/validate Antrean {:tiket "A-012" :nomor "dua belas" :layanan "ktp" :prioritas true}))
(println "kunci tidak ada :" (m/validate Antrean {:tiket "A-012" :nomor 12 :prioritas false}))
text Keluaran
data lengkap    : true
tipe salah      : false
kunci tidak ada : false

Data kedua ditolak karena :nomor berisi string, sedangkan data ketiga ditolak karena :layanan tidak ada. Perbandingan berikut merangkum perbedaan pendekatan spec.alpha dan malli untuk kasus yang sama.

Aspekspec.alphamalli
Tempat definisis/def mendaftarkan spec pada keyword globalSchema adalah nilai data yang dikirim sebagai argumen
Pemeriksaans/valid? atas nama specm/validate atas nilai schema
Diagnosis errors/explain dengan kunci laporan bernaungm/explain beserta me/humanize
Transformasi masukans/conform mengikuti specm/decode dengan transformer eksplisit

Dengan spec, nama spec menjadi titik temu yang digunakan di banyak tempat, pendekatan yang cocok untuk kontrak domain yang berumur panjang. Dengan malli, schema mengikuti aliran data dan mudah dipasangkan pada tepi sistem, misalnya validasi permintaan sebelum masuk handler.

Menjelaskan error dengan m/explain#

m/explain mengembalikan laporan berupa data ketika pemeriksaan gagal, dan laporan itu tetap bernilai nil ketika data sah. Setiap error membawa :path lokasi dalam schema, :in lokasi dalam data, :schema yang dilanggar, serta :value yang ditolak. Namespace malli.error menyediakan me/humanize yang merangkum laporan menjadi pesan per kunci, bentuk yang mudah dibaca manusia.

clojure Contoh: m/explain dan me/humanize
(require '[malli.core :as m]
         '[malli.error :as me])

(def Antrean
  [:map
   [:tiket string?]
   [:nomor int?]
   [:layanan [:enum "ktp" "kk" "akta"]]
   [:prioritas boolean?]])

(prn (:errors (m/explain Antrean {:tiket "A-012" :nomor "dua belas" :layanan "ktp" :prioritas true})))
(prn (me/humanize (m/explain Antrean {:tiket "A-012" :nomor "dua belas" :layanan "ktp" :prioritas true})))
text Keluaran
({:path [:nomor], :in [:nomor], :schema int?, :value "dua belas"})
{:nomor ["should be an int"]}

Baris pertama memperlihatkan satu error dengan lintasan yang lengkap, mulai dari path schema sampai nilai yang ditolak, sehingga program dapat memprosesnya lebih lanjut. Baris kedua adalah keluaran me/humanize yang menyandingkan kunci data dengan pesan singkatnya. Pesan bawaan humanize berbahasa Inggris, dan malli menyediakan mekanisme pesan khusus apabila bahasa lain diperlukan. Untuk diagnosis cepat saat pengembangan, humanize biasanya cukup, sedangkan laporan mentah m/explain cocok untuk log terstruktur.

Transformasi masukan dengan m/decode#

Data dari tepi sistem jarang datang dalam tipe yang benar. Nilai dari parameter URL, formulir, atau berkas konfigurasi biasanya berupa string, sedangkan program menuntut bilangan dan boolean. m/decode menerapkan transformer, pengubah nilai yang aturannya menyatu dengan schema, terhadap data masukan. Transformer string bawaan, mt/string-transformer, mengubah string menjadi tipe yang diminta schema, dan pemanggilan decode diikuti pemeriksaan m/validate untuk memastikan hasilnya benar-benar sah.

clojure Contoh: m/decode dengan string-transformer
(require '[malli.core :as m]
         '[malli.transform :as mt])

(def Antrean
  [:map
   [:tiket string?]
   [:nomor int?]
   [:layanan [:enum "ktp" "kk" "akta"]]
   [:prioritas boolean?]])

(def masukan
  {:tiket "A-012" :nomor "12" :layanan "ktp" :prioritas "true"})

(println "sebelum:" (pr-str masukan))
(println "sesudah:" (pr-str (m/decode Antrean masukan mt/string-transformer)))
(println "valid? :" (m/validate Antrean (m/decode Antrean masukan mt/string-transformer)))
text Keluaran
sebelum: {:tiket "A-012", :nomor "12", :layanan "ktp", :prioritas "true"}
sesudah: {:tiket "A-012", :nomor 12, :layanan "ktp", :prioritas true}
valid? : true

String "12" menjadi bilangan 12 dan string "true" menjadi boolean true, sedangkan :tiket dan :layanan tetap string karena schema memintanya demikian. Pola decode lalu validate ini lazim pada batas sistem, dan transformer lain seperti mt/json-transformer mengikuti pola yang sama untuk data JSON. Malli juga menyediakan m/coerce yang menggabungkan transformasi dengan pelemparan error apabila hasilnya tidak sah, sehingga pilihan antara decode dan coerce bergantung pada gaya penanganan error yang digunakan program.

Validator terkompilasi dan orientasi performa#

Malli menyusun schema menjadi fungsi validasi yang dapat digunakan ulang. Fungsi m/validator menerima schema sekali, lalu mengembalikan predikat siap pakai yang tidak lagi menyentuh struktur schema pada setiap pemanggilan. Pola ini menempatkan pekerjaan penyusunan di awal dan membuat panggilan berikutnya sekecil mungkin. Pendekatan ini sejalan dengan deskripsi proyek malli, yaitu pustaka schema berbasis data yang berorientasi transformasi dan performa.

clojure Contoh: validator terkompilasi dengan m/validator
(require '[malli.core :as m])

(def Antrean
  [:map
   [:tiket string?]
   [:nomor int?]
   [:layanan [:enum "ktp" "kk" "akta"]]
   [:prioritas boolean?]])

(def antrean-valid? (m/validator Antrean))

(println "A-012 :" (antrean-valid? {:tiket "A-012" :nomor 12 :layanan "ktp" :prioritas true}))
(println "B-007 :" (antrean-valid? {:tiket "B-007" :nomor 7 :layanan "kk" :prioritas false}))
text Keluaran
A-012 : true
B-007 : true

Predikat antrean-valid? kini dapat dipanggil pada ribuan entri antrean tanpa menyusun ulang schema, dan pemisahan ini terlihat dari kode yang tidak lagi menyebut Antrean pada setiap pemeriksaan. Pada pemanggilan langsung m/validate, penyusunan validator dilakukan setiap kali dipanggil, pola yang tidak masalah untuk pemeriksaan sesekali tetapi boros untuk jalur kode yang dieksekusi berulang kali. Aturan praktisnya sederhana: panggil m/validate saat eksplorasi, ikat m/validator saat schema digunakan berulang.

Kesalahan umum#

  • Menulis mt/string alih-alih mt/string-transformer. Transformer bawaan malli bernama mt/string-transformer dan mt/json-transformer, sedangkan nama pendek tersebut tidak ada dan memunculkan error simbol tidak ditemukan. Periksa namespace malli.transform apabila nama transformer diragukan.
  • Menganggap m/decode sekaligus memvalidasi. Decode hanya mengubah tipe sesuai aturan transformer dan dapat membiarkan data salah lolos. Panggil m/validate setelah decode, atau gunakan m/coerce apabila kegagalan diperlakukan sebagai error.
  • Memanggil m/validate pada jalur yang sering dieksekusi tanpa m/validator. Setiap pemanggilan menyusun ulang schema sehingga pekerjaan yang sama dilakukan berulang. Ikat predikat hasil m/validator dengan def dan panggil predikat itu untuk data dalam jumlah besar.

Ringkasan#

  • Schema malli adalah data biasa yang dikirim sebagai argumen, sehingga deskripsi bentuk data dapat didefinisikan di dekat penggunaannya tanpa pendaftaran global.
  • m/validate memeriksa kesahihan, m/explain menghasilkan laporan berbasis data, dan me/humanize merangkum laporan menjadi pesan per kunci.
  • m/decode dengan transformer seperti mt/string-transformer mengubah masukan string menjadi tipe yang diminta schema, dan hasilnya tetap perlu divalidasi.
  • m/validator menyusun schema menjadi predikat siap pakai, pendekatan kompilasi yang menjadi inti orientasi performa malli.

Referensi#

  • malli — Metosin — Repositori resmi malli beserta dokumentasi schema, transformer, validator, dan pesan error.