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.