Implementasi Inti

Pembangunan loop inti mesin konfigurasi yang berfungsi sebagai fondasi proyek.

Daftar isi

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.

lua Bentuk skema pada versi pertama
-- 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.

lua mesin.lua
-- 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 mesin

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

lua demo.lua
-- 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)
text Keluaran
== 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         Catatan

Berkas itu dijalankan dengan perintah berikut, dan keluarannya sama dengan yang ditunjukkan di atas.

bash
# jalankan demonstrasi mesin konfigurasi versi pertama
lua demo.lua
text Keluaran
== 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         Catatan

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

lua Contoh: pelanggaran tipe dan batas
-- 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)
end
text Keluaran
NILAI_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 bulat

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

  • SKEMA menjadi satu-satunya sumber aturan kunci, yaitu tipe, nilai bawaan, dan batas minimum, sehingga validasi bersifat generik.
  • muat_konfigurasi tidak 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, atau NILAI_DI_LUAR_BATAS.
  • tampilkan menyajikan laporan ringkas dengan pemformatan kolom yang sejajar.

Referensi#