Lapisan Berbasis Data

Penggunaan table sebagai konfigurasi dan konten proyek.

Daftar isi

Topik ini memindahkan konfigurasi dan konten proyek ke berkas data berbentuk table bernama konfigurasi.lua. Grup tema menyimpan pilihan tampilan, grup aturan menyimpan kandidat aturan penyuntingan, dan grup konten menyimpan label antarmuka beserta pintasan papan ketik. Mesin dari inti proyek digunakan kembali dengan penyempurnaan kecil, yaitu batas maksimum pada kunci bilangan bulat dan pengangkatan error terstruktur bila kandidat bukan table, sedangkan demonstrasi menunjukkan resolusi konfigurasi final yang bersumber dari berkas data tersebut.

Berkas data berbentuk table#

Berkas berikut menyimpan seluruh data konfigurasi dan konten proyek dalam bentuk table bersarang.

lua konfigurasi.lua
-- konfigurasi.lua
-- Berkas data konfigurasi dan konten aplikasi Pencatat.
-- Berkas ini hanya berisi data table dan tidak memuat logika apa pun.

local konfigurasi = {}

-- Preferensi tema, yaitu nama tema, warna aksen, dan ukuran teks antarmuka.
konfigurasi.tema = {
  nama = "terang",
  warna = "#f7f7f7",
  ukuran_teks = 15,
}

-- Aturan penyuntingan yang diusulkan berkas ini sebagai kandidat konfigurasi.
konfigurasi.aturan = {
  batas_panjang = 500,
}

-- Label antarmuka dan daftar pintasan papan ketik.
konfigurasi.konten = {
  label = "Catatan Harian",
  pintasan = {
    simpan = "Ctrl+S",
    catatan_baru = "Ctrl+N",
    cari = "Ctrl+F",
  },
}

return konfigurasi

Grup tema menyimpan nama tema terpilih, warna aksen, dan ukuran teks, sehingga host mengganti tampilan dengan membaca tiga kunci tersebut. Grup aturan berupa kandidat konfigurasi yang diusulkan berkas ini, yaitu batas panjang catatan 500 karakter. Grup konten menyimpan label antarmuka dan daftar pintasan papan ketik, sehingga teks antarmuka tidak tertanam di dalam kode host.

Penggunaan table sebagai berkas data menghilangkan penguraian format. Host tidak memerlukan pengurai JSON maupun format konfigurasi khusus, karena Lua sudah menguraikan table pada saat berkasnya dimuat. Struktur bersarang tersaji secara alami, dan seluruh data tetap berada dalam satu berkas yang mudah dibaca manusia.

Struktur konfigurasi menurut skema#

Enam kunci konfigurasi mengikuti skema yang ditetapkan inti mesin, dan perannya dirangkum pada tabel berikut.

KunciTipeDefaultKeterangan
tema.namastringgelapnama tema tampilan yang dipilih
tema.warnastring#1e1e2ewarna aksen tema dalam notasi heksadesimal
tema.ukuran_teksinteger14ukuran teks antarmuka, minimum 8
aturan.batas_panjanginteger1000batas panjang catatan dalam karakter, minimum 1
aturan.pola_berkasstringcatatan%d+pola penamaan berkas catatan yang dianggap sah
konten.labelstringCatatanlabel antarmuka pada judul aplikasi

Resolusi konfigurasi final bekerja dengan prinsip nilai bawaan dan penimpaan (override). Kunci yang tidak disebut pada kandidat menggunakan nilai bawaan skema, sedangkan kunci bernilai sah menimpanya. Pada berkas data di atas, nilai terang, #f7f7f7, 15, 500, dan Catatan Harian menimpa nilai bawaannya, sedangkan aturan.pola_berkas tidak disebut sehingga tetap menggunakan nilai bawaan catatan%d+.

Grup konten.pintasan sengaja berada di luar skema mesin. Pintasan merupakan konten, bukan konfigurasi perilaku, sehingga host membacanya langsung dari table data tanpa melewati validasi mesin. Pemisahan ini menjaga skema tetap ringkas, dan pemisahan semacam ini juga menunjukkan bahwa satu berkas data dapat melayani dua konsumen yang berbeda, yaitu mesin untuk konfigurasi dan host untuk konten.

Mesin yang digunakan kembali#

Berkas data ini diproses oleh mesin versi terbaru dari inti proyek. Salinan mesin tersebut menyertai topik ini agar bundle berdiri sendiri. Versi ini menyempurnakan versi pertama pada dua hal, yaitu batas maksimum pada kunci bilangan bulat (ukuran_teks sampai 32 dan batas_panjang sampai 100000) serta pengangkatan error terstruktur berbentuk table { kode = "KANDIDAT_TIDAK_SAH", pesan = "..." } bila kandidat yang diterima bukan table. Kedua penyempurnaan tersebut menjadi dasar pelaksanaan terlindungi di tepi lapisan skrip.

lua mesin.lua
-- mesin.lua
-- Inti mesin konfigurasi aplikasi Pencatat.
-- Versi ini memuat skema enam kunci, validasi, dan resolusi konfigurasi final.

local mesin = {}

-- Skema konfigurasi, yaitu tiga grup kunci sah beserta tipe, default, dan batas.
local SKEMA = {
  tema = {
    nama        = { tipe = "string",  default = "gelap" },
    warna       = { tipe = "string",  default = "#1e1e2e" },
    ukuran_teks = { tipe = "integer", default = 14, minimum = 8, maksimum = 32 },
  },
  aturan = {
    batas_panjang = { tipe = "integer", default = 1000, minimum = 1, maksimum = 100000 },
    pola_berkas   = { tipe = "string",  default = "catatan%d+" },
  },
  konten = {
    label = { tipe = "string", default = "Catatan" },
  },
}

-- Nama tipe yang ditampilkan pada pesan pelanggaran.
local function nama_tipe(tipe)
  if tipe == "integer" then
    return "bilangan bulat"
  end
  return tipe
end

-- Nama tipe aktual dari sebuah nilai untuk keperluan pesan pelanggaran.
local function nama_tipe_nilai(nilai)
  local t = type(nilai)
  if t == "number" then
    return math.type(nilai) == "integer" and "bilangan bulat" or "bilangan pecahan"
  end
  return t
end

-- Memeriksa kesesuaian tipe nilai terhadap aturan skemanya.
local function tipe_sesuai(aturan, nilai)
  if aturan.tipe == "integer" then
    return type(nilai) == "number" and math.type(nilai) == "integer"
  end
  return type(nilai) == aturan.tipe
end

-- Memvalidasi satu nilai dan mengembalikan teks pelanggaran atau nil.
local function validasi_kunci(jalur, aturan, nilai)
  if not tipe_sesuai(aturan, nilai) then
    return string.format(
      "TIPE_SALAH: kunci %s seharusnya bertipe %s, tetapi bertipe %s",
      jalur, nama_tipe(aturan.tipe), nama_tipe_nilai(nilai)
    )
  end
  if aturan.tipe == "integer" and (nilai < aturan.minimum or nilai > aturan.maksimum) then
    return string.format(
      "NILAI_DI_LUAR_BATAS: kunci %s seharusnya bernilai antara %d dan %d",
      jalur, aturan.minimum, aturan.maksimum
    )
  end
  return nil
end

-- Menggabungkan nilai default skema dengan kandidat konfigurasi.
-- Kandidat tidak pernah dimodifikasi, hasilnya selalu table baru.
function mesin.muat_konfigurasi(kandidat)
  if type(kandidat) ~= "table" then
    error({ kode = "KANDIDAT_TIDAK_SAH",
      pesan = string.format(
        "kandidat konfigurasi seharusnya table, tetapi bertipe %s", type(kandidat)
      ) }, 0)
  end
  local final = {}
  local pelanggaran = {}
  for nama_grup, skema_grup in pairs(SKEMA) do
    final[nama_grup] = {}
    local grup = kandidat[nama_grup]
    if grup ~= nil and type(grup) ~= "table" then
      table.insert(pelanggaran, string.format(
        "TIPE_SALAH: grup %s seharusnya table, tetapi bertipe %s",
        nama_grup, nama_tipe_nilai(grup)))
      grup = nil
    end
    for nama_kunci, aturan in pairs(skema_grup) do
      local nilai = nil
      if grup ~= nil then
        nilai = grup[nama_kunci]
      end
      if nilai == nil then
        final[nama_grup][nama_kunci] = aturan.default
      else
        local temuan = validasi_kunci(nama_grup .. "." .. nama_kunci, aturan, nilai)
        if temuan == nil then
          final[nama_grup][nama_kunci] = nilai
        else
          table.insert(pelanggaran, temuan)
          final[nama_grup][nama_kunci] = aturan.default
        end
      end
    end
  end
  for nama_grup, grup in pairs(kandidat) do
    if SKEMA[nama_grup] == nil then
      table.insert(pelanggaran, string.format(
        "KUNCI_TAK_DIKENAL: grup %s tidak dikenal", nama_grup))
    elseif type(grup) == "table" then
      for nama_kunci in pairs(grup) do
        if SKEMA[nama_grup][nama_kunci] == nil then
          table.insert(pelanggaran, string.format(
            "KUNCI_TAK_DIKENAL: kunci %s.%s tidak dikenal", nama_grup, nama_kunci))
        end
      end
    end
  end
  table.sort(pelanggaran)
  return final, pelanggaran
end

-- Mencetak laporan ringkas konfigurasi final dengan urutan yang tetap.
function mesin.tampilkan(final)
  print(string.format("%-22s %s", "tema.nama", final.tema.nama))
  print(string.format("%-22s %s", "tema.warna", final.tema.warna))
  print(string.format("%-22s %s", "tema.ukuran_teks", final.tema.ukuran_teks))
  print(string.format("%-22s %s", "aturan.batas_panjang", final.aturan.batas_panjang))
  print(string.format("%-22s %s", "aturan.pola_berkas", final.aturan.pola_berkas))
  print(string.format("%-22s %s", "konten.label", final.konten.label))
end

return mesin

Mesin menerima table utuh dari berkas data sebagai kandidatnya, bukan hanya grup aturan. Akibatnya, kunci konten.pintasan yang tidak ada pada skema dilaporkan sebagai pelanggaran kecil, dan pelaporan semacam itu wajar karena mesin memberitahukan bahwa kunci tersebut tidak diprosesnya. Nilai lainnya tetap diresolusi normal, sehingga satu pelanggaran konten tidak menggagalkan seluruh konfigurasi. Apabila kandidatnya bukan table, mesin mengangkat error terstruktur sebagai gantinya, dan cara menangkapnya dibahas pada komponen permukaan error proyek ini.

Cara host membaca berkas data#

Pemakaian berkas data dapat dilihat dari dua sudut pandang. Host yang ditulis dalam Lua cukup memanggil dofile("konfigurasi.lua") dan menerima table utuh sebagai nilai balik. Host C memuat berkas yang sama dengan luaL_dofile, sehingga nilai baliknya berada di puncak stack, lalu membaca subtable dan kuncinya dengan lua_getfield. Potongan berikut menggambarkan sudut pandang kedua pada level model, karena kompilasinya memerlukan kepala lua.h dan pustaka Lua yang tidak tersedia di lingkungan latihan.

c Contoh: membaca table konfigurasi dari host C
// tidak-dijalankan
/* muat berkas data, nilai baliknya berupa table di puncak stack */
if (luaL_dofile(L, "konfigurasi.lua") != LUA_OK) {
    fprintf(stderr, "berkas data rusak: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
    return 1;
}

/* baca subtable aturan, lalu salah satu kuncinya */
lua_getfield(L, -1, "aturan");
lua_getfield(L, -1, "batas_panjang");
int batas = (int)lua_tointeger(L, -1);
lua_pop(L, 3);  /* lepas kunci, subtable, dan table utama */

Urutan pembacaan mengikuti sifat stack, yaitu setiap lua_getfield mendorong satu nilai ke puncak, sehingga lua_pop perlu melepas seluruh nilai yang sudah selesai dibaca. Pemanggilan sekali muat semacam ini cukup untuk seluruh sesi aplikasi, karena data konfigurasi tidak berubah selagi aplikasi berjalan.

Demonstrasi resolusi berbasis data#

Berkas demo.lua memuat mesin dan berkas data, lalu memproses table dari berkas data tersebut sebagai kandidat konfigurasi dan menampilkan pintasan yang langsung dibaca host.

lua demo.lua
-- demo.lua
-- Demonstrasi resolusi konfigurasi yang bersumber dari berkas data.

local mesin = dofile("mesin.lua")
local konfigurasi = dofile("konfigurasi.lua")

-- Berkas data diserahkan utuh sebagai kandidat konfigurasi.
local final, pelanggaran = mesin.muat_konfigurasi(konfigurasi)

print("== pelanggaran yang dilaporkan mesin ==")
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)

-- Data di luar skema dibaca host langsung dari berkas data, tanpa mesin.
print("== pintasan yang dibaca host langsung ==")
print(string.format("%-14s %s", "simpan", konfigurasi.konten.pintasan.simpan))
print(string.format("%-14s %s", "catatan_baru", konfigurasi.konten.pintasan.catatan_baru))
print(string.format("%-14s %s", "cari", konfigurasi.konten.pintasan.cari))
text Keluaran
== pelanggaran yang dilaporkan mesin ==
KUNCI_TAK_DIKENAL: kunci konten.pintasan tidak dikenal
== konfigurasi final ==
tema.nama              terang
tema.warna             #f7f7f7
tema.ukuran_teks       15
aturan.batas_panjang   500
aturan.pola_berkas     catatan%d+
konten.label           Catatan Harian
== pintasan yang dibaca host langsung ==
simpan         Ctrl+S
catatan_baru   Ctrl+N
cari           Ctrl+F

Skrip dijalankan dengan perintah berikut, dan keluarannya persis seperti blok Keluaran di bawahnya.

bash
# jalankan demonstrasi resolusi berbasis data
lua demo.lua
text Keluaran
== pelanggaran yang dilaporkan mesin ==
KUNCI_TAK_DIKENAL: kunci konten.pintasan tidak dikenal
== konfigurasi final ==
tema.nama              terang
tema.warna             #f7f7f7
tema.ukuran_teks       15
aturan.batas_panjang   500
aturan.pola_berkas     catatan%d+
konten.label           Catatan Harian
== pintasan yang dibaca host langsung ==
simpan         Ctrl+S
catatan_baru   Ctrl+N
cari           Ctrl+F

Keluaran menunjukkan pembagian peran yang dirancang. Mesin melaporkan konten.pintasan sebagai kunci yang tidak dikenalnya, tetapi tetap meresolusi lima kunci konfigurasi lainnya dengan benar, yaitu tema terang dengan warna #f7f7f7 dan ukuran teks 15, batas panjang 500, pola bawaan catatan%d+, serta label Catatan Harian. Sementara itu, host membaca tiga pintasan langsung dari berkas data tanpa melewati mesin. Dua konsumen bekerja dari satu sumber data yang sama tanpa saling mengganggu.

Ringkasan#

  • konfigurasi.lua menyimpan tema, aturan, dan konten sebagai table bersarang, sehingga host memuat datanya tanpa menguraikan format apa pun.
  • Resolusi konfigurasi menggabungkan nilai bawaan skema dengan kandidat, yaitu kunci yang tidak disebut menggunakan nilai bawaan dan kunci sah menimpanya.
  • Konten seperti pintasan berada di luar skema mesin dan dibaca host secara langsung dari table data.
  • Host Lua memuat berkas data dengan dofile, sedangkan host C membaca nilai baliknya dari puncak stack dengan lua_getfield.

Referensi#