İçeriğe geç
academia.sh

Ders 23 / 34

Makine Okunur Belgeler

Sözleşmenin şema olarak yazılması, aynı tanımdan hem doğrulayıcı hem insan okur belge üretilmesi ve sunucunun ürettiği yanıt ile tanımın ayrıştığı durumun çalışma anında yakalanması.

İçindekiler

Buraya kadar kurulan her şey — hata biçimi, alan düzeyi doğrulama, sürümler, kırıcılık kuralları, kullanımdan kaldırma penceresi — sözleşmenin parçasıdır. Sözleşmenin nerede yazılı olduğu ise açık kalmıştır. Düzyazıyla yazılırsa iki şey yapılamaz: makineye verilemez ve gerçeklikle karşılaştırılamaz.

Fark alıcı zaten bir şema üzerinde çalışıyordu. Bu ders o şemayı sözleşmenin tek kaynağı hâline getirir: aynı tanımdan doğrulayıcı ve belge üretilir, sonra sunucunun gerçekten ürettiği yanıt aynı tanıma karşı sınanır.

Sözleşmenin Şema Olarak Yazılması

Gövde şemaları için JSON Schema kullanılır; tip, zorunluluk, izin verilen değerler ve desen bu sözcük dağarcığıyla ifade edilir. Şemaları saran yol/yöntem/yanıt düzeni ise arayüz tanım biçimlerinin (OpenAPI) yaptığı işi yapar: hangi yolun hangi yöntemi desteklediğini ve her durum kodu için hangi gövdenin döneceğini bildirir.

// tanim.mjs — odunc servisinin makine okunur tanimi
// Govde semalari JSON Schema sozcukleriyle yazilmistir; disindaki sarmal, arayuz
// tanim bicimlerinin (OpenAPI) yol/yontem/yanit duzenini yansitir.

const ODUNC_ISTEK = {
  type: "object",
  required: ["uye", "kalemler"],
  additionalProperties: false,
  properties: {
    uye: { type: "string", pattern: "^U-\\d{4}$" },
    kalemler: {
      type: "array",
      items: { type: "object", required: ["isbn"], additionalProperties: false,
               properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$" } } },
    },
    sube: { type: "string", enum: ["merkez", "sahil", "tepe"] },
  },
};

const ODUNC_YANIT = {
  type: "object",
  required: ["id", "uye", "kalemler", "iadeTarihi", "durum"],
  additionalProperties: false,
  properties: {
    id: { type: "string", pattern: "^O-\\d+$" },
    uye: { type: "string" },
    kalemler: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false,
                properties: { isbn: { type: "string" } } } },
    iadeTarihi: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
    durum: { type: "string", enum: ["acik", "kapali"] },
  },
};

const SORUN = {
  type: "object",
  required: ["type", "title", "status", "detail", "instance"],
  properties: {
    type: { type: "string" }, title: { type: "string" }, status: { type: "integer" },
    detail: { type: "string" }, instance: { type: "string" },
  },
};

export const TANIM = {
  "POST /odunc": { istek: ODUNC_ISTEK, yanit: { 201: ODUNC_YANIT, 422: SORUN } },
  "GET /odunc/{id}": { istek: null, yanit: { 200: ODUNC_YANIT, 404: SORUN } },
};

Tanımın hata yanıtlarını da kapsadığına dikkat edilmeli. Sözleşme yalnız başarı gövdesini tanımlarsa, istemci hata durumunda ne bekleyeceğini yine bilemez; ilk derste kurulan problem ayrıntısı biçimi de burada şema olarak yazılır ve böylece denetlenebilir hâle gelir.

Tanımdan Doğrulayıcı Üretmek

Şemanın makine okunur olması, ondan bir doğrulayıcı üretilebilmesi demektir. Aşağıdaki program JSON Schema’nın kullanılan alt kümesini yorumlar ve her şema için bir doğrulama işlevi döndürür.

// dogrulayici.mjs — JSON Schema alt kumesinden dogrulayici uretir
// Desteklenen sozcukler: type, required, properties, items, enum, pattern, additionalProperties

const tipi = (d) =>
  d === null ? "null" : Array.isArray(d) ? "array"
    : typeof d === "number" ? (Number.isInteger(d) ? "integer" : "number") : typeof d;

function sina(sema, deger, yol, hatalar) {
  const t = tipi(deger);
  if (sema.type && t !== sema.type && !(sema.type === "number" && t === "integer")) {
    hatalar.push(`${yol || "/"}: ${sema.type} bekleniyordu, ${t} geldi`);
    return;
  }
  if (sema.enum && !sema.enum.includes(deger)) hatalar.push(`${yol}: ${JSON.stringify(deger)} izin verilen değerlerde yok`);
  if (sema.pattern && !new RegExp(sema.pattern).test(String(deger))) hatalar.push(`${yol}: ${JSON.stringify(deger)} desene uymuyor`);

  if (sema.type === "object") {
    for (const ad of sema.required ?? []) if (!(ad in deger)) hatalar.push(`${yol}/${ad}: zorunlu alan yok`);
    if (sema.additionalProperties === false) {
      for (const ad of Object.keys(deger)) {
        if (!(ad in (sema.properties ?? {}))) hatalar.push(`${yol}/${ad}: tanımda olmayan alan`);
      }
    }
    for (const [ad, alt] of Object.entries(sema.properties ?? {})) {
      if (ad in deger) sina(alt, deger[ad], `${yol}/${ad}`, hatalar);
    }
  }
  if (sema.type === "array" && sema.items) deger.forEach((o, i) => sina(sema.items, o, `${yol}/${i}`, hatalar));
}

// Semadan bir dogrulayici islev uretir: (deger) -> hata listesi
export const dogrulayiciUret = (sema) => (deger) => {
  const hatalar = [];
  sina(sema, deger, "", hatalar);
  return hatalar;
};

Hata iletilerinin gövde yolunu taşıdığına dikkat edilmeli: doğrulama hataları dersinde kurulan alan adlandırma sözleşmesi burada kendiliğinden korunur, çünkü yol şemanın gezilmesi sırasında üretilir. Şema tek kaynak olduğunda, alan adları da tek kaynaktan gelir.

Aynı tanımdan insan okur belge de üretilir. Belge ile doğrulayıcının aynı dosyadan gelmesi, belgelendirme sapmasını yapısal olarak imkânsız kılar.

// belge.mjs — dogrulayiciyi ureten tanimdan insan okur belge uretir
import { TANIM } from "./tanim.mjs";

const nitelik = (s) => [
  s.type,
  s.enum ? `değerler: ${s.enum.join("|")}` : null,
  s.pattern ? `desen: ${s.pattern}` : null,
].filter(Boolean).join(", ");

function yaz(sema, girinti = "  ") {
  if (sema.type === "object") {
    for (const [ad, alt] of Object.entries(sema.properties ?? {})) {
      const zorunlu = (sema.required ?? []).includes(ad) ? "zorunlu" : "isteğe bağlı";
      console.log(`${girinti}${ad.padEnd(12)} ${zorunlu.padEnd(12)} ${nitelik(alt)}`);
      if (alt.type === "object" || alt.type === "array") yaz(alt.type === "array" ? alt.items : alt, girinti + "  ");
    }
    if (sema.additionalProperties === false) console.log(`${girinti}(tanımda olmayan alan kabul edilmez)`);
  }
}

const SUZGEC = process.argv[2];   // istege bagli: yalniz bu anahtar yazilir

for (const [anahtar, t] of Object.entries(TANIM)) {
  if (SUZGEC && anahtar !== SUZGEC) continue;
  console.log(`\n## ${anahtar}`);
  if (t.istek) { console.log(" istek gövdesi:"); yaz(t.istek); }
  for (const [kod, sema] of Object.entries(t.yanit)) { console.log(` yanıt ${kod}:`); yaz(sema); }
}
node belge.mjs "POST /odunc"
## POST /odunc
 istek gövdesi:
  uye          zorunlu      string, desen: ^U-\d{4}$
  kalemler     zorunlu      array
    isbn         zorunlu      string, desen: ^97[89]-\d{10}$
    (tanımda olmayan alan kabul edilmez)
  sube         isteğe bağlı string, değerler: merkez|sahil|tepe
  (tanımda olmayan alan kabul edilmez)
 yanıt 201:
  id           zorunlu      string, desen: ^O-\d+$
  uye          zorunlu      string
  kalemler     zorunlu      array
    isbn         zorunlu      string
    (tanımda olmayan alan kabul edilmez)
  iadeTarihi   zorunlu      string, desen: ^\d{4}-\d{2}-\d{2}$
  durum        zorunlu      string, değerler: acik|kapali
  (tanımda olmayan alan kabul edilmez)
 yanıt 422:
  type         zorunlu      string
  title        zorunlu      string
  status       zorunlu      integer
  detail       zorunlu      string
  instance     zorunlu      string

Yanıtın Tanıma Karşı Denetlenmesi

İsteği doğrulamak yaygın bir uygulamadır. Asıl sapma, sunucunun ürettiği yanıtta oluşur: bir alan eklenir, bir değer kümesi genişler, bir alan bazı durumlarda dönmemeye başlar. Bunların hiçbiri isteği doğrulayarak yakalanmaz.

Aşağıdaki sunucu yanıtı yazan tek bir noktaya sahiptir ve o noktada yanıtı da tanıma karşı sınar. Sapmalar bir listeye kaydedilir ve /denetim yolundan okunur. İçine bilerek bir sapma konmuştur: O-1 kaydı için tanımda olmayan bir alan ve tanımda olmayan bir durum değeri üretilir.

// sunucu.mjs — istegi tanimdan uretilen dogrulayiciyla siner, yaniti da tanima karsi denetler
import { createServer } from "node:http";
import { TANIM } from "./tanim.mjs";
import { dogrulayiciUret } from "./dogrulayici.mjs";

// Tanimdaki her sema icin bir kez dogrulayici uretilir.
const ISTEK_DOGRULAYICI = Object.fromEntries(
  Object.entries(TANIM).filter(([, t]) => t.istek).map(([a, t]) => [a, dogrulayiciUret(t.istek)]));
const YANIT_DOGRULAYICI = Object.fromEntries(
  Object.entries(TANIM).flatMap(([a, t]) => Object.entries(t.yanit).map(([k, s]) => [`${a} ${k}`, dogrulayiciUret(s)])));

const sapmalar = [];   // yanit ile tanimin ayristigi yerler

const oduncler = new Map([["O-1", { id: "O-1", uye: "U-1001", kalemler: [{ isbn: "978-0262033848" }], iadeTarihi: "2026-03-20", durum: "acik" }]]);
let sayac = 1;

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

// Yanit yazan tek nokta: tanima karsi denetim burada yapilir.
const yanitla = (yanit, anahtar, kod, govde, tip = "application/json") => {
  const dogrula = YANIT_DOGRULAYICI[`${anahtar} ${kod}`];
  const hatalar = dogrula ? dogrula(govde) : ["tanımda bu yanıt kodu yok"];
  if (hatalar.length) sapmalar.push({ anahtar, kod, hatalar });
  yanit.writeHead(kod, { "content-type": `${tip}; charset=utf-8` });
  yanit.end(JSON.stringify(govde));
};

const sorun = (tur, baslik, kod, ayrinti) =>
  ({ type: `https://ornek.kutuphane/sorunlar/${tur}`, title: baslik, status: kod, detail: ayrinti, instance: `ol-${sayac++}` });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const yol = istek.url.split("?")[0];

  if (yol === "/denetim") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify(sapmalar, null, 1));
  }

  if (istek.method === "POST" && yol === "/odunc") {
    const anahtar = "POST /odunc";
    const govde = JSON.parse((await govdeOku(istek)) || "{}");
    const hatalar = ISTEK_DOGRULAYICI[anahtar](govde);
    if (hatalar.length) {
      const g = sorun("dogrulama", "İstek gövdesi doğrulanamadı", 422, hatalar.join("; "));
      return yanitla(yanit, anahtar, 422, g, "application/problem+json");
    }
    const kayit = { id: `O-${oduncler.size + 1}`, uye: govde.uye, kalemler: govde.kalemler, iadeTarihi: "2026-04-15", durum: "acik" };
    oduncler.set(kayit.id, kayit);
    return yanitla(yanit, anahtar, 201, kayit);
  }

  if (istek.method === "GET" && yol.startsWith("/odunc/")) {
    const anahtar = "GET /odunc/{id}";
    const kayit = oduncler.get(yol.slice("/odunc/".length));
    if (!kayit) return yanitla(yanit, anahtar, 404, sorun("kaynak-yok", "Kaynak bulunamadı", 404, `${yol} yok.`), "application/problem+json");
    // SAPMA: gecikmis kayitlar icin tanimda olmayan alan ve deger uretiliyor.
    const cikti = kayit.id === "O-1"
      ? { ...kayit, durum: "gecikmis", gecikmeGunu: 12 }
      : kayit;
    return yanitla(yanit, anahtar, 200, cikti);
  }

  yanit.writeHead(404, { "content-type": "application/problem+json; charset=utf-8" });
  yanit.end(JSON.stringify(sorun("kaynak-yok", "Kaynak bulunamadı", 404, `${yol} yok.`)));
}).listen(8437, "127.0.0.1", () => console.log("sunucu 127.0.0.1:8437"));
#!/usr/bin/env bash
# Gecerli istek, gecersiz istek ve tanimla ayrisan yanit; sonra denetim dokumu.
node sunucu.mjs & s=$!
sleep 0.5

echo "--- gecerli istek ---"
curl -sS -w '  [%{http_code}]\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"U-1002","kalemler":[{"isbn":"978-0201896831"}],"sube":"sahil"}' http://127.0.0.1:8437/odunc

echo "--- gecersiz istek ---"
curl -sS -w '  [%{http_code}]\n' -X POST -H 'content-type: application/json' \
  -d '{"uye":"1002","kalemler":[{"isbn":"0201896831","adet":2}],"sube":"deniz","not":"acele"}' http://127.0.0.1:8437/odunc

echo "--- tanimla ayrisan yanit ---"
curl -sS -w '  [%{http_code}]\n' http://127.0.0.1:8437/odunc/O-1

echo "--- denetim ---"
curl -sS http://127.0.0.1:8437/denetim

kill "$s"; wait "$s" 2>/dev/null
sunucu 127.0.0.1:8437
--- gecerli istek ---
{"id":"O-2","uye":"U-1002","kalemler":[{"isbn":"978-0201896831"}],"iadeTarihi":"2026-04-15","durum":"acik"}  [201]
--- gecersiz istek ---
{"type":"https://ornek.kutuphane/sorunlar/dogrulama","title":"İstek gövdesi doğrulanamadı","status":422,"detail":"/not: tanımda olmayan alan; /uye: \"1002\" desene uymuyor; /kalemler/0/adet: tanımda olmayan alan; /kalemler/0/isbn: \"0201896831\" desene uymuyor; /sube: \"deniz\" izin verilen değerlerde yok","instance":"ol-1"}  [422]
--- tanimla ayrisan yanit ---
{"id":"O-1","uye":"U-1001","kalemler":[{"isbn":"978-0262033848"}],"iadeTarihi":"2026-03-20","durum":"gecikmis","gecikmeGunu":12}  [200]
--- denetim ---
[
 {
  "anahtar": "GET /odunc/{id}",
  "kod": 200,
  "hatalar": [
   "/gecikmeGunu: tanımda olmayan alan",
   "/durum: \"gecikmis\" izin verilen değerlerde yok"
  ]
 }
]

Geçersiz istek beş ayrı ihlalle reddedildi ve hiçbir kural elle yazılmadı; hepsi tanımdan geldi. Asıl önemli olan üçüncü satırdır: sunucu tanımda olmayan bir alan ve tanımda olmayan bir durum değeri içeren bir yanıt üretti, istemci bu yanıtı 200 kodu ve tam gövdeyle aldı, ama sapma kayda geçti.

Bu, bir önceki dersteki kırıcılık sınıflandırmasının çalışma anındaki karşılığıdır. gecikmis değerinin eklenmesi, yanıt yönünde değer kümesinin genişlemesidir ve hoşgörü bildirimi yoksa kırıcıdır. Fark alıcı bunu iki şema karşılaştırıldığında bulur; buradaki denetim ise şema hiç güncellenmeden kodun değiştiği durumu bulur. İkisi farklı kaçakları kapatır: biri bilinçli değişikliği sınıflandırır, diğeri bilinçsiz değişikliği görünür kılar.

Denetimin Engelleyici Olmaması

Yanıt tanımdan saptığında iki davranış seçilebilir. Denetim engelleyici olursa sunucu kendi yanıtını reddedip 500 üretir; kaydedici olursa yanıtı gönderir ve sapmayı kütüğe yazar. Yukarıdaki sunucu ikincisini yapar.

Seçim, sapmanın kime zarar verdiğine bakar. Tanımda olmayan bir alanın eklenmesi çoğu istemci için zararsızdır; o yanıtı engellemek, çalışan bir işlevi kullanılamaz hâle getirir. Buna karşılık geliştirme ve sınama ortamlarında engelleyici kip yerindedir: sapma üretim ortamına ulaşmadan kırılır. Aynı denetim, ortama göre iki farklı davranışla çalıştırılır.

Kaydedici kipin bir koşulu vardır: sapma listesinin okunması. Kimsenin bakmadığı bir kütüğe yazılan sapma, hiç saptanmamış sapmayla aynı şeydir.

Özet

  • Sözleşme düzyazıyla yazıldığında makineye verilemez ve gerçeklikle karşılaştırılamaz; şema olarak yazıldığında ikisi de yapılabilir.
  • Gövde şemaları JSON Schema ile, yol/yöntem/yanıt düzeni arayüz tanım biçimlerinin yaptığı işi yapan bir sarmalla yazılır ve hata yanıtlarını da kapsar.
  • Aynı tanımdan hem doğrulayıcı hem insan okur belge üretildiğinde belgelendirme sapması yapısal olarak olanaksızlaşır; alan adları da tek kaynaktan gelir.
  • Yalnız isteği doğrulamak bir boşluk bırakır: sapma çoğunlukla sunucunun ürettiği yanıtta oluşur ve ancak yanıt da tanıma karşı sınanırsa yakalanır.
  • Yanıt denetimi, şema hiç güncellenmeden kodun değiştiği durumu bulur; fark alıcı ise bilinçli olarak değiştirilmiş iki şemayı sınıflandırır.
  • Denetim üretim ortamında kaydedici, geliştirme ve sınama ortamında engelleyici çalıştırılır; kaydedici kip yalnızca sapma listesi okunuyorsa iş görür.

Sonraki Adım

Tanım artık sunucunun gerçekten ne ürettiğini denetliyor, ama tek bir soruyu yanıtsız bırakıyor: tüketicinin gerçekten kullandığı alanlar hangileri? Tanım kalemler dizisinin her öğesinde isbn bulunduğunu söyler; raf terminali bu alanı okuyor mudur, yoksa yalnız id ve durum mu ona yetiyor? Bu bilinmeden hangi değişikliğin kimi bozacağı kestirilemez ve her değişiklik en kötü duruma göre planlanır. Sonraki ders tüketici beklentilerini dosyaya yazar, sağlayıcıya karşı çalıştırır ve sağlayıcı kırıcı bir değişiklik yaptığında hangi tüketicinin sınamasının düştüğünü gösterir.

İ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