Topik ini membangun versi pertama mesin.lua, yaitu inti proyek yang menangani validasi berbasis skema, penerapan nilai bawaan, dan resolusi konfigurasi final. Fungsi muat_konfigurasi(kandidat) menggabungkan kandidat konfigurasi dengan skema, lalu mengembalikan table konfigurasi final beserta daftar pelanggarannya, sedangkan fungsi tampilkan(final) menyajikan laporan ringkasnya. Demonstrasi pada bagian akhir menjalankan kedua fungsi tersebut melalui demo.lua, sehingga perilaku mesin dapat diperiksa langsung pada terminal.
Skema sebagai sumber aturan#
Validasi berbasis skema menempatkan seluruh aturan kunci pada satu table bernama SKEMA. Skema disimpan sebagai table bersarang yang mengikuti struktur kandidat, yaitu grup tema, aturan, dan konten, dengan setiap kunci pada daftarnya memetakan tipe yang diwajibkan, nilai bawaan, dan batas minimum bila kuncinya berupa bilangan bulat. Penulisan bersarang membuat jalur kunci terbaca, misalnya tema.ukuran_teks yang berarti kunci ukuran_teks pada grup tema. Bentuk skema pada versi pertama ini diperlihatkan pada potongan berikut.
-- tiga grup kunci sah beserta tipe, default, dan batas minimumnya
local SKEMA = {
tema = {
nama = { tipe = "string", default = "gelap" },
warna = { tipe = "string", default = "#1e1e2e" },
ukuran_teks = { tipe = "integer", default = 14, minimum = 8 },
},
aturan = {
batas_panjang = { tipe = "integer", default = 1000, minimum = 1 },
pola_berkas = { tipe = "string", default = "catatan%d+" },
},
konten = {
label = { tipe = "string", default = "Catatan" },
},
}Skema menjadi satu-satunya tempat perubahan ketika lingkup proyek menambah kunci. Fungsi validasinya bersifat generik, yaitu membaca aturan dari skema alih-alih menguji setiap kunci secara manual dalam kode. Pendekatan ini juga menyeragamkan pesan pelanggaran, karena setiap pesan dibentuk dari pola yang sama pada satu tempat. Nilai bawaan pada skema memastikan kandidat yang tidak menyebut sebuah kunci tetap menghasilkan konfigurasi final yang lengkap, sehingga pemanggil tidak pernah menerima table yang setengah terisi. Skema juga diekspor melalui mesin.SKEMA agar bagian pengujian proyek dapat memeriksanya tanpa membaca berkas sumbernya.
Versi pertama mesin.lua#
Berkas berikut memuat seluruh inti versi pertama, yaitu skema, validasi per kunci, resolusi konfigurasi final, dan laporan ringkasnya. Setiap pesan pelanggaran diawali kodenya, yaitu KUNCI_TAK_DIKENAL untuk kunci yang tidak ada pada skema, TIPE_SALAH untuk nilai dengan tipe yang salah, dan NILAI_DI_LUAR_BATAS untuk nilai di bawah batas minimumnya.
-- mesin.lua
-- Inti mesin konfigurasi aplikasi Pencatat.
-- Versi pertama: skema, validasi per kunci, dan resolusi konfigurasi final.
local mesin = {}
-- Skema konfigurasi, yaitu kunci sah beserta tipenya dan nilai defaultnya.
-- Struktur skema mengikuti struktur table kandidat, yaitu bersarang per grup.
local SKEMA = {
tema = {
nama = { tipe = "string", default = "gelap" },
warna = { tipe = "string", default = "#1e1e2e" },
ukuran_teks = { tipe = "integer", default = 14, minimum = 8 },
},
aturan = {
batas_panjang = { tipe = "integer", default = 1000, minimum = 1 },
pola_berkas = { tipe = "string", default = "catatan%d+" },
},
konten = {
label = { tipe = "string", default = "Catatan" },
},
}
mesin.SKEMA = SKEMA
-- Menyatakan tipe sebuah nilai dengan kata yang mudah dibaca.
local function sebut_tipe(nilai)
local t = math.type(nilai)
if t == "integer" then
return "bilangan bulat"
elseif t == "float" then
return "bilangan desimal"
else
return type(nilai)
end
end
-- Memvalidasi satu nilai terhadap aturan skemanya.
-- Mengembalikan teks pelanggaran atau nil bila nilai sah.
local function validasi_kunci(jalur, aturan, nilai)
local sah = false
if aturan.tipe == "string" then
sah = type(nilai) == "string"
else
sah = math.type(nilai) == aturan.tipe
end
if not sah then
return string.format(
"TIPE_SALAH: kunci %s seharusnya bertipe %s, tetapi menerima %s",
jalur, aturan.tipe, sebut_tipe(nilai)
)
end
if aturan.minimum ~= nil and nilai < aturan.minimum then
return string.format(
"NILAI_DI_LUAR_BATAS: kunci %s bernilai %d, di bawah batas minimum %d",
jalur, nilai, aturan.minimum
)
end
return nil
end
-- Menggabungkan nilai default dengan kandidat konfigurasi.
-- Kandidat tidak pernah dimodifikasi, hasilnya selalu table baru.
function mesin.muat_konfigurasi(kandidat)
if type(kandidat) ~= "table" then
kandidat = {}
end
local final = {}
local pelanggaran = {}
-- Nilai kandidat yang sah mengisi table final, sisanya diisi default.
for nama_grup, skema_grup in pairs(SKEMA) do
final[nama_grup] = {}
for nama_kunci, aturan in pairs(skema_grup) do
local jalur = nama_grup .. "." .. nama_kunci
local nilai = nil
if type(kandidat[nama_grup]) == "table" then
nilai = kandidat[nama_grup][nama_kunci]
end
if nilai == nil then
final[nama_grup][nama_kunci] = aturan.default
else
local temuan = validasi_kunci(jalur, aturan, nilai)
if temuan ~= nil then
table.insert(pelanggaran, temuan)
final[nama_grup][nama_kunci] = aturan.default
else
final[nama_grup][nama_kunci] = nilai
end
end
end
end
-- Kunci kandidat yang tidak ada pada skema dilaporkan, bukan diabaikan.
for nama_grup, nilai_grup in pairs(kandidat) do
if SKEMA[nama_grup] == nil then
table.insert(pelanggaran,
"KUNCI_TAK_DIKENAL: kunci " .. nama_grup .. " tidak dikenal")
elseif type(nilai_grup) ~= "table" then
table.insert(pelanggaran, string.format(
"TIPE_SALAH: kunci %s seharusnya berupa table, tetapi menerima %s",
nama_grup, sebut_tipe(nilai_grup)))
else
for nama_kunci in pairs(nilai_grup) do
if SKEMA[nama_grup][nama_kunci] == nil then
table.insert(pelanggaran,
"KUNCI_TAK_DIKENAL: kunci " .. nama_grup .. "." .. nama_kunci .. " tidak dikenal")
end
end
end
end
table.sort(pelanggaran)
return final, pelanggaran
end
-- Mencetak laporan ringkas konfigurasi final.
function mesin.tampilkan(final)
print(string.format("%-20s %s", "tema.nama", final.tema.nama))
print(string.format("%-20s %s", "tema.warna", final.tema.warna))
print(string.format("%-20s %s", "tema.ukuran_teks", final.tema.ukuran_teks))
print(string.format("%-20s %s", "aturan.batas_panjang", final.aturan.batas_panjang))
print(string.format("%-20s %s", "aturan.pola_berkas", final.aturan.pola_berkas))
print(string.format("%-20s %s", "konten.label", final.konten.label))
end
return mesinKeputusan desain pada versi pertama ini dapat dirangkum menjadi tiga hal. Pertama, kandidat tidak pernah dimodifikasi langsung, karena muat_konfigurasi selalu membangun table final yang baru. Data milik pemanggil tetap utuh, sehingga pemanggil bebas menggunakan kandidatnya kembali untuk percobaan lain tanpa khawatir kuncinya berubah.
Kedua, pelanggaran dikumpulkan seluruhnya alih-alih menghentikan eksekusi pada pelanggaran pertama. Satu eksekusi muat_konfigurasi melaporkan setiap kunci bermasalah sekaligus, sehingga perbaikan konfigurasi tidak perlu berulang-ulang satu per satu. Nilai yang melanggar tetap diganti nilai bawaannya, sehingga pengumpulan pelanggaran tidak menghalangi host menerima konfigurasi final yang lengkap. Daftar pelanggaran juga diurutkan dengan table.sort agar laporannya stabil dan mudah dibandingkan antareksekusi.
Ketiga, kunci yang tidak dikenal dilaporkan alih-alih diabaikan diam-diam. Kunci baku yang salah tulis, misalnya ukuran_teks yang tertulis ukuran_text, segera terlihat pada daftar pelanggaran. Pemeriksaannya berlapis, yaitu nama grup yang tidak ada pada skema dilaporkan pada tingkat grup, sedangkan kunci yang salah di dalam grup yang sah dilaporkan dengan jalur lengkapnya, misalnya tema.warna_latar.
Urutan pemeriksaan pada validasi_kunci juga disengaja. Tipe diperiksa terlebih dahulu, karena perbandingan batas terhadap nilai bertipe string justru menimbulkan error perbandingan pada Lua. Setelah tipe terbukti benar, pemeriksaan batas bilangan berjalan dengan aman, dan pesan pelanggaran dapat menyebut tipe yang diharapkan beserta tipe yang diterima. Pemeriksaan tipe bilangan bulat menggunakan math.type, sehingga nilai desimal seperti 14.5 juga dilaporkan sebagai TIPE_SALAH dan tidak lolos menjadi ukuran teks.
Demonstrasi resolusi konfigurasi#
Berkas demo.lua memuat mesin dengan dofile, memproses satu kandidat konfigurasi sebagian, lalu menampilkan laporannya. Kandidatnya berisi kunci sah untuk nama tema, ukuran teks, dan batas panjang, ditambah satu kunci tidak dikenal pada grup tema untuk menunjukkan pelanggaran.
-- demo.lua
-- Demonstrasi pemakaian mesin konfigurasi versi pertama.
local mesin = dofile("mesin.lua")
-- Kandidat konfigurasi sebagian dengan satu kunci tidak dikenal.
local kandidat = {
tema = {
nama = "terang",
ukuran_teks = 16,
warna_latar = "#fafafa",
},
aturan = {
batas_panjang = 2500,
},
}
local final, pelanggaran = mesin.muat_konfigurasi(kandidat)
print("== pelanggaran ==")
if #pelanggaran == 0 then
print("tidak ada pelanggaran")
else
for _, temuan in ipairs(pelanggaran) do
print(temuan)
end
end
print("== konfigurasi final ==")
mesin.tampilkan(final)== pelanggaran ==
KUNCI_TAK_DIKENAL: kunci tema.warna_latar tidak dikenal
== konfigurasi final ==
tema.nama terang
tema.warna #1e1e2e
tema.ukuran_teks 16
aturan.batas_panjang 2500
aturan.pola_berkas catatan%d+
konten.label CatatanBerkas itu dijalankan dengan perintah berikut, dan keluarannya sama dengan yang ditunjukkan di atas.
# jalankan demonstrasi mesin konfigurasi versi pertama
lua demo.lua== pelanggaran ==
KUNCI_TAK_DIKENAL: kunci tema.warna_latar tidak dikenal
== konfigurasi final ==
tema.nama terang
tema.warna #1e1e2e
tema.ukuran_teks 16
aturan.batas_panjang 2500
aturan.pola_berkas catatan%d+
konten.label CatatanKeluaran memperlihatkan dua hal. Pada kandidat pengguna, kunci tema.nama, tema.ukuran_teks, dan aturan.batas_panjang diterima, sedangkan tema.warna_latar dilaporkan sebagai kunci tidak dikenal dan tidak masuk ke konfigurasi final. Kunci yang tidak disebut oleh kandidat diisi nilai bawaannya, misalnya tema.warna yang berisi #1e1e2e dan konten.label yang berisi Catatan. Fungsi tampilkan merapikan laporan dengan pemformatan %-20s, sehingga nilai setiap kunci mulai pada kolom yang sama, dan laporan semacam ini mudah dibaca pada terminal maupun log.
Pelanggaran tipe dan batas#
Pelanggaran tipe dan pelanggaran batas diperagakan dengan kandidat lain. Kandidat berikut memuat tema.ukuran_teks bernilai 5, yaitu di bawah batas minimum 8, dan aturan.pola_berkas bertipe bilangan, padahal skema menuntut string. Kedua pelanggaran muncul dalam satu eksekusi karena mesin mengumpulkan seluruh temuan alih-alih berhenti pada yang pertama, sebagaimana dijelaskan pada bagian sebelumnya.
-- Memperagakan pelanggaran tipe dan pelanggaran batas pada satu kandidat.
local mesin = dofile("mesin.lua")
local kandidat = {
tema = {
nama = "gelap",
ukuran_teks = 5,
},
aturan = {
pola_berkas = 42,
},
}
local _, pelanggaran = mesin.muat_konfigurasi(kandidat)
for _, temuan in ipairs(pelanggaran) do
print(temuan)
endNILAI_DI_LUAR_BATAS: kunci tema.ukuran_teks bernilai 5, di bawah batas minimum 8
TIPE_SALAH: kunci aturan.pola_berkas seharusnya bertipe string, tetapi menerima bilangan bulatKedua pesan memuat kode pelanggarannya beserta jalur kunci lengkap, sehingga lokasi masalah tidak perlu dicari secara manual. Nilai yang melanggar tidak pernah ikut ke konfigurasi final, karena mesin menggantinya dengan nilai bawaan skema pada saat yang sama dengan pencatatan pelanggarannya. Perilaku ini menegaskan prinsip fail-safe yang dirancang pada bagian arsitektur. Kode KUNCI_TAK_DIKENAL tidak muncul pada contoh ini karena seluruh kunci kandidatnya memang ada pada skema.
Kesalahan umum#
Kesalahan pertama yang lazim adalah memodifikasi kandidat secara langsung, misalnya mengisi kunci yang hilang pada table kandidat itu sendiri. Perubahan diam-diam semacam ini membuat pemanggil kehilangan data aslinya, dan pembandingan kandidat sebelum serta sesudah pemanggilan akan menemukan perbedaan yang tidak disengaja. Perbaikannya sudah diterapkan pada versi ini, yaitu table final selalu dibangun baru dan kandidatnya tidak disentuh.
Kesalahan kedua adalah melintasi SKEMA dengan ipairs. Fungsi ipairs hanya melintasi bagian array table, sedangkan SKEMA berbentuk pemetaan kunci bernama, sehingga entri seperti tema dapat terlewat tanpa pesan apa pun. Perbaikannya adalah pairs, yang melintasi seluruh kunci table pemetaan, sebagaimana digunakan pada muat_konfigurasi.
Kesalahan ketiga adalah memperlakukan kandidat nil sebagai kegagalan. Pemanggil tanpa preferensi sama sekali adalah keadaan yang sah, sehingga perlakuannya adalah kandidat kosong yang menghasilkan seluruh nilai bawaan, dan bukan pesan error. Kesalahan keempat adalah membiarkan daftar pelanggaran tidak diurutkan. Urutan pelintasan pairs bergantung pada keadaan internal penerjemah, sehingga laporan dua eksekusi pada skrip yang sama dapat berbeda urutan. Pemanggilan table.sort pada akhir resolusi menghapus ketidakpastian tersebut.
Ringkasan#
SKEMAmenjadi satu-satunya sumber aturan kunci, yaitu tipe, nilai bawaan, dan batas minimum, sehingga validasi bersifat generik.muat_konfigurasitidak pernah mengubah kandidat dan selalu mengembalikan table final yang baru, lengkap dengan nilai bawaan untuk kunci yang tidak disebut.- Pelanggaran dikumpulkan seluruhnya, diurutkan agar stabil, dan diawali kodenya, yaitu
KUNCI_TAK_DIKENAL,TIPE_SALAH, atauNILAI_DI_LUAR_BATAS. tampilkanmenyajikan laporan ringkas dengan pemformatan kolom yang sejajar.
Referensi#
- Manual Referensi Lua 5.4 — Lua.org
— Bagian 6 tentang pustaka standar menjadi rujukan perilaku
type,pairs,math.type,string.format, dantable.sortpada inti mesin. - Programming in Lua, edisi keempat — Lua.org — Buku rujukan table dan konstruksi data yang menjadi dasar penyusunan skema konfigurasi.