İçeriğe geç
academia.sh

Ders 19 / 34

Doğrulama Hataları

Alan düzeyinde hata listesinin problem ayrıntısına eklenmesi, ilk hatada durmakla hepsini toplamanın istek turu farkı ve alan adlarının istemcideki giriş kutularıyla eşleşmesini sağlayan gövde yolu kuralı.

İçindekiler

Bir önceki ders hata gövdesini tek biçime bağladı ama sorun kataloğundaki dogrulama türünü kullanmadan bıraktı. Nedeni, bu türün diğerlerinden yapısal olarak farklı olmasıdır. “Kopya yok” tek bir olgudur; detail alanındaki bir cümle onu tümüyle anlatır. “İstek gövdesi doğrulanamadı” ise bir olgu değil, bir olgular kümesidir: gövdenin hangi alanları, hangi kurallara göre reddedildi?

Ödünç servisinin ödünç verme isteği artık tek kitaplık değildir; bir üye, birden çok kalem ve bir iade tarihi taşır. Bu ders alan düzeyinde hata bildirimini kurar ve iki kararı ölçerek verir: hataların kaçının bir seferde bildirileceği ve alanların hangi adla anılacağı.

Bir Seferde Kaç Hata

Doğrulama, ilk kuralı çiğneyen alanda durabilir ya da bütün gövdeyi tarayıp hataları toplayabilir. Aradaki fark, kullanıcının formu düzeltmek için kaç kez istek yollayacağıdır.

Aşağıdaki doğrulayıcı şemayı veri olarak alır. Şema anahtarları gövde yoludur ve JSON Pointer biçiminde yazılır: /uye, /kalemler/1/isbn. * işareti dizi öğeleri üzerinde açılır, yani /kalemler/*/isbn kalıbı gövdedeki her kaleme uygulanır.

// dogrula.mjs — govdeyi semaya gore dogrular, alan duzeyinde hata listesi uretir
// Alan adi JSON Pointer (RFC 6901): "/uye", "/kalemler/1/isbn"

export const KURALLAR = {
  zorunlu: (d) => (d === undefined || d === null || d === "" ? { kod: "zorunlu" } : null),
  desen: (d, p) => (new RegExp(p).test(String(d)) ? null : { kod: "bicim", param: p }),
  secenek: (d, p) => (p.includes(d) ? null : { kod: "secenek_disi", param: p }),
  dizi: (d, p) =>
    !Array.isArray(d) ? { kod: "dizi_degil" }
      : d.length < p.enAz ? { kod: "cok_az", param: p }
      : d.length > p.enCok ? { kod: "cok_cok", param: p }
      : null,
  tarih: (d) => (/^\d{4}-\d{2}-\d{2}$/.test(String(d)) ? null : { kod: "tarih_degil" }),
  enCokGun: (d, p, bugun) => {
    const fark = Math.round((Date.parse(d) - Date.parse(bugun)) / 86400000);
    if (Number.isNaN(fark)) return null;                 // tarih kurali zaten bildirdi
    if (fark < 0) return { kod: "gecmiste" };
    return fark > p ? { kod: "cok_ileri", param: p } : null;
  },
};

// Yol icindeki * gercek dizi dizinleriyle acilir: /kalemler/*/isbn -> /kalemler/0/isbn
const oku = (govde, yol) =>
  yol.split("/").slice(1).reduce((d, p) => (d == null ? undefined : d[p]), govde);

function yollariAc(govde, kalip) {
  const yildiz = kalip.indexOf("/*");
  if (yildiz < 0) return [kalip];
  const dizi = oku(govde, kalip.slice(0, yildiz));
  if (!Array.isArray(dizi)) return [];
  return dizi.flatMap((_, i) => yollariAc(govde, kalip.slice(0, yildiz) + `/${i}` + kalip.slice(yildiz + 2)));
}

// dogrula(sema, govde, bugun) -> [{ yol, kod, param }]
export function dogrula(sema, govde, bugun) {
  const hatalar = [];
  for (const [kalip, kurallar] of Object.entries(sema)) {
    for (const yol of yollariAc(govde, kalip)) {
      const deger = oku(govde, yol);
      for (const [ad, param] of kurallar) {
        if (ad !== "zorunlu" && (deger === undefined || deger === null || deger === "")) break;
        const sonuc = KURALLAR[ad](deger, param, bugun);
        if (sonuc) { hatalar.push({ yol, ...sonuc }); break; }  // alan basina tek hata
      }
    }
  }
  return hatalar;
}

İki toplama biçimini karşılaştırmak için, dört alanı birden yanlış olan bir gövde alınır ve kullanıcının yalnızca kendisine bildirilen hataları düzelttiği varsayılır.

// tur-tur.mjs — ilk hatada duran dogrulama ile hepsini toplayan dogrulamayi karsilastirir
import { dogrula } from "./dogrula.mjs";

const BUGUN = "2026-03-01";
const SEMA = {
  "/uye":             [["zorunlu"], ["desen", "^U-\\d{4}$"]],
  "/kalemler":        [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]],
  "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]],
  "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]],
  "/iadeTarihi":      [["zorunlu"], ["tarih"], ["enCokGun", 30]],
};

// Kullanicinin ilk gonderdigi govde: dort alan birden yanlis.
const ILK = {
  uye: "1001",
  kalemler: [
    { isbn: "978-0262033848", sube: "merkez" },
    { isbn: "0262033848", sube: "deniz" },
  ],
  iadeTarihi: "2026-06-01",
};

// Kullanicinin duzeltme sirasi: hangi hata bildirilirse o duzeltilir.
const DUZELTME = {
  "/uye": (g) => (g.uye = "U-1001"),
  "/kalemler/1/isbn": (g) => (g.kalemler[1].isbn = "978-0201896831"),
  "/kalemler/1/sube": (g) => (g.kalemler[1].sube = "sahil"),
  "/iadeTarihi": (g) => (g.iadeTarihi = "2026-03-20"),
};

function turSayisi(hepsiniTopla) {
  const govde = structuredClone(ILK);
  let tur = 0;
  while (tur < 10) {
    tur++;
    const hatalar = dogrula(SEMA, govde, BUGUN);
    if (hatalar.length === 0) return tur;
    const bildirilen = hepsiniTopla ? hatalar : [hatalar[0]];
    console.log(`  tur ${tur}: ${bildirilen.map((h) => `${h.yol}=${h.kod}`).join("  ")}`);
    for (const h of bildirilen) DUZELTME[h.yol](govde);
  }
  return tur;
}

console.log("ilk hatada duran doğrulama:");
const a = turSayisi(false);
console.log(`  başarılı olana kadar istek turu: ${a}\n`);

console.log("hepsini toplayan doğrulama:");
const b = turSayisi(true);
console.log(`  başarılı olana kadar istek turu: ${b}`);
ilk hatada duran doğrulama:
  tur 1: /uye=bicim
  tur 2: /kalemler/1/isbn=bicim
  tur 3: /kalemler/1/sube=secenek_disi
  tur 4: /iadeTarihi=cok_ileri
  başarılı olana kadar istek turu: 5

hepsini toplayan doğrulama:
  tur 1: /uye=bicim  /kalemler/1/isbn=bicim  /kalemler/1/sube=secenek_disi  /iadeTarihi=cok_ileri
  başarılı olana kadar istek turu: 2

Beş tura karşı iki tur. Sayının kendisi gövdedeki yanlış alan sayısına bağlıdır — nn yanlış alan, ilk hatada duran doğrulamada n+1n+1 tur demektir — ama davranış farkı sayıdan bağımsızdır: ilk hatada durmak, kullanıcıya bir sonraki hatanın varlığını saklar. Kullanıcı her düzeltmeden sonra işin bittiğini sanır ve yeni bir ret alır.

Bir ayrıntı korunmuştur: doğrulayıcı alan başına ilk hatada durur. /uye alanı hem zorunlu hem desene uygun olmalıdır; boş bırakıldığında “zorunlu” ve “biçim” hatalarının ikisini birden bildirmenin bir yararı yoktur, ikincisi birincinin sonucudur. Kural şudur: alanlar arasında tarama sürer, alan içinde ilk hata yeter.

Hata Kaydının Üç Alanı

Her hata kaydı üç şey taşır. yol, gövdedeki alanın adresidir. kod, hatanın kararlı kimliğidir — Frontend müfredatındaki Uygulama Mimarisi kursunda kurulan doğrulama şemalarıyla aynı ayrım: kod dallanmak içindir, ileti göstermek için. param, kodun gerektirdiği sayıdır: hangi desen, hangi seçenekler, kaç gün.

Kodun ileti yerine geçmesi, kullanıcıya gösterilecek metni istemci tarafında üretmeyi mümkün kılar. Sunucu cok_ileri ve 30 gönderir; istemci bunu kendi dilinde ve kendi tonunda cümleye çevirir. Sunucu metin gönderseydi çeviri, çoğul kuralı ve ton kararı sunucuya taşınırdı; hiçbiri sunucunun işi değildir.

Bu üçlü, problem ayrıntısına bir ek üye olarak eklenir. Çekirdek beş alan yerinde kalır; errors dizisi onların yanına gelir.

// sunucu.mjs — odunc istegini dogrulayip 422 problem ayrintisi donduren servis
import { createServer } from "node:http";
import { dogrula } from "./dogrula.mjs";

const BUGUN = "2026-03-01";                 // ornegin yinelenebilir olmasi icin sabit
const SEMA = {
  "/uye":             [["zorunlu"], ["desen", "^U-\\d{4}$"]],
  "/kalemler":        [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]],
  "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]],
  "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]],
  "/iadeTarihi":      [["zorunlu"], ["tarih"], ["enCokGun", 30]],
};

// Bir onceki derste yazilan sorun katalogunun bu derse dusen tek girdisi.
let sayac = 0;
const dogrulamaSorunu = (hatalar) => ({
  type: "https://ornek.kutuphane/sorunlar/dogrulama",
  title: "İstek gövdesi doğrulanamadı",
  status: 422,
  detail: `${hatalar.length} alan kabul edilmedi.`,
  instance: `ol-${String(++sayac).padStart(4, "0")}`,
  errors: hatalar,
});

const govdeOku = (istek) =>
  new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); });

// Cekirdek alanlar alt alta, her hata tek satir: govde okunabilir kalsin.
const bicimle = (s) => [
  "{",
  ...["type", "title", "status", "detail", "instance"].map((a) => `  "${a}": ${JSON.stringify(s[a])},`),
  '  "errors": [',
  ...s.errors.map((h, i) => `    ${JSON.stringify(h)}${i < s.errors.length - 1 ? "," : ""}`),
  "  ]",
  "}",
].join("\n");

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const govde = JSON.parse((await govdeOku(istek)) || "{}");
  const hatalar = dogrula(SEMA, govde, BUGUN);

  if (hatalar.length) {
    yanit.writeHead(422, { "content-type": "application/problem+json; charset=utf-8" });
    return yanit.end(bicimle(dogrulamaSorunu(hatalar)));
  }
  yanit.writeHead(201, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify({ id: "O-2", uye: govde.uye, kalem: govde.kalemler.length }));
}).listen(8433, "127.0.0.1", () => console.log("dogrulama sunucusu 127.0.0.1:8433"));
#!/usr/bin/env bash
# Dort alani yanlis bir odunc istegi yollar, 422 govdesini gosterir.
node sunucu.mjs & sunucu=$!
sleep 0.5

curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"0262033848","sube":"deniz"}],"iadeTarihi":"2026-06-01"}' \
  http://127.0.0.1:8433/odunc

echo "--- duzeltilmis govde ---"
curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"U-1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"978-0201896831","sube":"sahil"}],"iadeTarihi":"2026-03-20"}' \
  http://127.0.0.1:8433/odunc

kill "$sunucu"; wait "$sunucu" 2>/dev/null
dogrulama sunucusu 127.0.0.1:8433
{
  "type": "https://ornek.kutuphane/sorunlar/dogrulama",
  "title": "İstek gövdesi doğrulanamadı",
  "status": 422,
  "detail": "4 alan kabul edilmedi.",
  "instance": "ol-0001",
  "errors": [
    {"yol":"/uye","kod":"bicim","param":"^U-\\d{4}$"},
    {"yol":"/kalemler/1/isbn","kod":"bicim","param":"^97[89]-\\d{10}$"},
    {"yol":"/kalemler/1/sube","kod":"secenek_disi","param":["merkez","sahil","tepe"]},
    {"yol":"/iadeTarihi","kod":"cok_ileri","param":30}
  ]
}
[422] application/problem+json; charset=utf-8
--- duzeltilmis govde ---
{"id":"O-2","uye":"U-1001","kalem":2}
[201] application/json; charset=utf-8

Durum kodunun 400 değil 422 olması bilinçli bir seçimdir. 400, isteğin sunucu tarafından anlaşılamadığını bildirir; bozuk JSON, eksik başlık, tanınmayan içerik tipi. Buradaki gövde eksiksiz anlaşılmıştır — ayrıştırılmış, alanları okunmuş, kurallara sokulmuştur — ve anlamı reddedilmiştir. Ayrım kütüğe yansıdığında da işe yarar: 400 oranındaki artış istemcinin isteği yanlış kurduğunu, 422 oranındaki artış kullanıcının yanlış veri girdiğini gösterir; ikisinin nedeni ve çözümü farklıdır.

Alan Adının İstemcideki Karşılığı

Hata listesinin işe yaraması, istemcinin her hatayı hangi giriş kutusunun yanında göstereceğini bilmesine bağlıdır. Erişilebilir Bileşen Kalıpları kursundaki hata sunumu bu eşleşmeyi varsayar: her hata kendi alanının ek açıklamasına bağlanır, hata özeti odağı o alana taşır. Eşleşme kurulamazsa geriye tek bir genel ileti kalır.

Aşağıdaki ölçüm aynı dört hatayı üç ayrı sunucu sözleşmesiyle bildirir ve istemcinin kaçını kutusuyla eşleştirebildiğini sayar.

// eslesme.mjs — uc ayri alan adlandirma sozlesmesinin istemcide eslesme oranini olcer
// Istemcideki giris kutulari, sozlesmedeki kurala gore adlandirilmistir:
// ad = JSON Pointer'in bas egik cizgisi atilmis, "/" yerine "." konmus hali.
const KUTULAR = ["uye", "kalemler.0.isbn", "kalemler.0.sube", "kalemler.1.isbn", "kalemler.1.sube", "iadeTarihi"];
const adaCevir = (yol) => yol.replace(/^\//, "").replaceAll("/", ".");

// Ayni dort hata, uc ayri sunucu sozlesmesiyle bildirilmis hali.
const SOZLESMELER = {
  "alan adı yok": {
    hatalar: [{ ileti: "Gönderdiğiniz bilgilerde dört hata var." }],
    cevir: () => null,
  },
  "sunucunun iç adları": {
    hatalar: [
      { alan: "member_id", kod: "bicim" },
      { alan: "items[1].isbn", kod: "bicim" },
      { alan: "items[1].branch", kod: "secenek_disi" },
      { alan: "due_date", kod: "cok_ileri" },
    ],
    cevir: (h) => h.alan,
  },
  "gövde yolu (JSON Pointer)": {
    hatalar: [
      { yol: "/uye", kod: "bicim" },
      { yol: "/kalemler/1/isbn", kod: "bicim" },
      { yol: "/kalemler/1/sube", kod: "secenek_disi" },
      { yol: "/iadeTarihi", kod: "cok_ileri" },
    ],
    cevir: (h) => adaCevir(h.yol),
  },
};

console.log("sözleşme                    bildirilen  eşleşen  kutusunun yanında gösterilebilen");
for (const [ad, { hatalar, cevir }] of Object.entries(SOZLESMELER)) {
  const eslesen = hatalar.filter((h) => KUTULAR.includes(cevir(h)));
  console.log(
    `${ad.padEnd(27)} ${String(hatalar.length).padStart(9)}  ${String(eslesen.length).padStart(7)}  ` +
    `${eslesen.length ? eslesen.map((h) => cevir(h)).join(", ") : "hiçbiri"}`
  );
}
sözleşme                    bildirilen  eşleşen  kutusunun yanında gösterilebilen
alan adı yok                        1        0  hiçbiri
sunucunun iç adları                 4        0  hiçbiri
gövde yolu (JSON Pointer)           4        4  uye, kalemler.1.isbn, kalemler.1.sube, iadeTarihi

İkinci satır ilkinden daha kötüdür, çünkü aldatıcıdır: sunucu dört ayrı hata bildirmiş görünür, alan adları da vardır, ama o adlar sunucunun kendi iç veri modelinden gelir. member_id, items[1].branch — istemcinin gövdesinde böyle bir alan yoktur. İstemci ya sunucunun iç adlarıyla kendi alan adları arasında elle bir çeviri tablosu tutar ya da hataları eşleştirmeden gösterir. Çeviri tablosu tutulursa sunucunun her iç yeniden adlandırması istemciyi bozar; yani sözleşmede olmayan bir şey sözleşme gibi davranmaya başlar.

Kural şudur: hata yolu, istemcinin gönderdiği gövdedeki alanın adresidir. Sunucunun iç model adları değil, istek gövdesinin yapısı. Bu kural iki özelliği garanti eder. Toplamdır — gövdedeki her alanın bir yolu vardır, dizi öğeleri dâhil. Ve tek yönlü türetilebilir: istemci gövdeyi kendisi kurduğu için yolu kutu adına çevirecek kuralı yazabilir, ters yönde bir tablo tutmasına gerek kalmaz.

Kalan tek karar, gövde yolunun hangi biçimde yazılacağıdır. JSON Pointer tanımlı bir standarttır, dizi dizinlerini doğal biçimde ifade eder ve kaçırma kuralları belirlidir. Yerine nokta ayırıcı da seçilebilir; belirleyici olan biçimin kendisi değil, belgelenmiş ve her alan için tanımlı olmasıdır.

Özet

  • Doğrulama ilk hatada durduğunda nn yanlış alan n+1n+1 istek turu gerektirir; hatalar toplu bildirildiğinde iki tur yeter ve kullanıcıdan bir sonraki hatanın varlığı saklanmaz.
  • Alanlar arasında tarama sürer, alan içinde ilk hata yeter: aynı alanın ardışık kural ihlalleri birbirinin sonucudur.
  • Hata kaydı üç alan taşır — yol, kod, parametre; kullanıcıya gösterilecek metin sunucudan değil koddan ve parametreden istemcide üretilir.
  • Anlaşılamayan istek 400, anlaşılıp anlamı reddedilen istek 422 ile bildirilir; ayrım kütükteki artışın nedenini de ayırır.
  • Hata yolu istemcinin gönderdiği gövdedeki adrestir, sunucunun iç model adı değil; iç adlar bildirildiğinde eşleşme oranı dörtte sıfıra düşer.
  • Alan adlandırma sözleşmesi toplam ve tek yönlü türetilebilir olmalıdır; hangi biçimin seçildiği değil, belgelenmiş ve her alan için tanımlı olması belirleyicidir.

Sonraki Adım

Doğrulama şeması bu derste sabit kabul edildi: /kalemler en çok üç öğe alır, iade tarihi en çok otuz gün ileri olabilir. Kütüphane bu sınırları değiştirmeye karar verdiğinde ne olacak? Kalem sınırının beşe çıkması hiçbir istemciyi bozmaz; üçe düşmesi, dört kalemlik istek yollayan her istemciyi bozar. Aynı gövde alanının adının değişmesi ise bütün istemcileri aynı anda bozar. Sözleşmenin değişmesi kaçınılmazdır; sonraki ders bu değişikliği yönetilebilir kılan aracı ele alır ve aynı kaynağın iki sürümünü tek sunucuda yayımlayarak yol, başlık ve içerik tabanlı sürümlemenin istemci koduna maliyetini karşılaştırır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat