Wings Tarif Kitabı
Wings ile gerçekten kurduğun şeyler için kısa,
kopyala-yapıştır tarifler. Aşağıdaki her parça eksiksiz bir programdır — bir
.tpr dosyasına koy ve tulpar app.tpr ile çalıştır. Her biri derlenip curl
ile denendi, yani istek/yanıt şekilleri birebir aldığın çıktı.
JSON CRUD API nasıl yaparım?
Section titled “JSON CRUD API nasıl yaparım?”Veriyi bir global’de tut ve ona yaz — Wings, global’e kök salan yazıları otomatik kalıcılaştırır, böylece in-memory “veritabanın” istekler arası yaşar.
import "wings";
json _todos = [{"id": 1, "title": "Wings öğren", "done": false}];int _next_id = 2;
func list_todos(req) { return ok(_todos); }
func get_todo(req) { int id = toInt(req["params"]["id"]); for (t in _todos) { if (t["id"] == id) { return ok(t); } } return not_found("todo yok");}
func create_todo(req) { json body = req["json"]; json t = {"id": _next_id, "title": body["title"], "done": false}; _next_id = _next_id + 1; push(_todos, t); return created(t);}
func delete_todo(req) { int id = toInt(req["params"]["id"]); json kept = []; for (t in _todos) { if (t["id"] != id) { push(kept, t); } } _todos = kept; return no_content();}
get("/todos", "list_todos");get("/todos/:id", "get_todo");post("/todos", "create_todo");del("/todos/:id", "delete_todo");serve(8080);GET /todos → [{"id":1,"title":"Wings öğren","done":false}]POST /todos → 201 {"id":2,"title":"...","done":false}GET /todos/2 → {"id":2,...}GET /todos/99 → 404 {"error":"todo yok"}DELETE /todos/2 → 204Route’ları middleware ile nasıl korurum?
Section titled “Route’ları middleware ile nasıl korurum?”use("fn") her handler’dan önce çalışan global bir middleware kaydeder. Bir
yanıt döndürmek (helper ile status set ederek) zinciri kısa-devre yapar;
{} döndürmek devam ettirir.
import "wings";
func require_token(req) { str auth = req["headers"]["Authorization"]; if (auth != "Bearer s3cret") { return unauthorized("geçerli token gerekli"); } return {};}
func secret(req) { return ok({"data": "gizli"}); }
use("require_token");get("/secret", "secret");serve(8080);GET /secret → 401GET /secret (Authorization: Bearer x) → 401GET /secret (Authorization: Bearer s3cret) → {"data":"gizli"}Route-başına auth (dependency injection) nasıl yaparım?
Section titled “Route-başına auth (dependency injection) nasıl yaparım?”Yalnızca bazı route’lar guard istediğinde — ve çözülen değeri handler içinde
kullanmak istediğinde — bir bağımlılık kullan. depends("fn") onu en son
kaydedilen route’a iliştirir; sonucu dep("name") ile okursun.
import "wings";
func current_user(req) { str t = req["headers"]["Authorization"]; if (length(t) == 0) { return unauthorized("token gerekli"); } return {"id": 1, "name": "Ada"};}
func profile(req) { json u = dep("current_user"); return ok({"hello": u["name"], "uid": u["id"]});}
get("/profile", "profile");depends("current_user");serve(8080);GET /profile → 401GET /profile (Authorization: tok) → {"hello":"Ada","uid":1}JWT authentication nasıl eklerim?
Section titled “JWT authentication nasıl eklerim?”Gerçek bir login akışı için — token’ı bir kez üret, sonra diğer tüm route’ları
onunla koru — wings_jwt paketini (token
imzalar/doğrular) Wings’in yerleşik jwt_guard middleware’iyle (her istekte
Authorization header’ını kontrol eder ve claim’leri req["jwt"]’e enjekte
eder) birleştir. Paketi bir kez kur:
tulpar pkg add wings_jwtimport "wings";import "wings_jwt" as jwt;
str SECRET = "dev-secret-degistir"; // gerçek deployment'ta env'den yükle
func login(req) { json b = req["json"]; if (b["username"] != "ada" || b["password"] != "s3cret") { return unauthorized("hatalı kimlik bilgisi"); } str token = jwt.sign_ttl({"sub": "1", "role": "admin"}, SECRET, 3600); return ok({"token": token});}
func me(req) { return ok({"sub": req["jwt"]["sub"], "role": req["jwt"]["role"]});}
post("/login", "login");get("/me", "me");
jwt_guard(SECRET); // yukarıda kayıtlı her şeyi koru...jwt_public("/login"); // ...bu hariçserve(8080);curl localhost:8080/me# → 401 (token yok)
curl -X POST -d '{"username":"ada","password":"s3cret"}' localhost:8080/login# → {"token":"eyJhbGciOiJIUzI1NiIs..."}
curl -H "Authorization: Bearer <token>" localhost:8080/me# → {"sub":"1","role":"admin"}jwt_guard, global bir use() middleware’i bağlar, yani kayıtlı her
handler’dan önce çalışır — o yüzden korumak istediğin tüm route’lar zaten
kayıtlıyken en sona çağır. jwt_public(path) bir path’i muaf tutar (tam eşleşme
ya da sondaki * ile prefix); /healthz, /metrics, /docs ve
/openapi.json varsayılan olarak muaftır. wings_jwt ve jwt_guard aynı wire
formatını konuşur (HS256, base64url segmentleri), yani login’de
jwt.sign_ttl ile imzalanan bir token, sonraki her istekte jwt_guard’a karşı
sorunsuz doğrulanır.
İstek gövdesini nasıl doğrularım?
Section titled “İstek gövdesini nasıl doğrularım?”body_schema() ile bir şema iliştir. Handler’dan önce çalışır, böylece
geçersiz bir gövde koduna hiç ulaşmaz — her hatalı alanı listeleyen otomatik bir
422 alır.
import "wings";
func register(req) { json b = req["json"]; return created({"name": b["name"], "email": b["email"]});}
post("/register", "register");body_schema({ "name": {"type": "str", "min": 2, "max": 40}, "email": {"type": "str", "regex": "^[^@]+@[^@]+$"}, "age?": {"type": "int", "min": 13, "max": 120} // sondaki ? = opsiyonel});serve(8080);POST {"name":"A","email":"nope"} → 422 {"error":"validation failed", "fields":{"name":"min length 2","email":"must match ^[^@]+@[^@]+$"}}POST {"name":"Ada","email":"a@b.io"} → 201Query param’larla sayfalama ve filtre nasıl yaparım?
Section titled “Query param’larla sayfalama ve filtre nasıl yaparım?”Tipli erişimciler ?page=2&desc=true’yu fallback ile coerce eder, böylece
sayfalamayı string parse etmeden tek satırda okursun.
import "wings";
json _items = [];
func list_items(req) { int page = query_int(req, "page", 1); int size = query_int(req, "size", 10); str sort = query(req, "sort", "id"); bool desc = query_bool(req, "desc", false); return ok({"page": page, "size": size, "sort": sort, "desc": desc});}
get("/items", "list_items");serve(8080);GET /items?page=3&size=25&sort=name&desc=true → {"page":3,"size":25,"sort":"name","desc":true}query_bool, 1/true/yes ve 0/false/no (büyük/küçük harf duyarsız)
kabul eder.
Dosya yüklemeyi nasıl alırım?
Section titled “Dosya yüklemeyi nasıl alırım?”Metin alanlarını form(...), dosyaları uploaded_files(...) ile oku. Dosya
data’sı ham byte’tır (binary-safe), yani byte-exact round-trip eder.
import "wings";
func upload(req) { str title = form(req, "title", "(başlıksız)"); json saved = []; for (f in uploaded_files(req)) { write_file("./uploads/" + f["filename"], f["data"]); push(saved, {"name": f["filename"], "size": f["size"]}); } return created({"title": title, "files": saved});}
post("/upload", "upload");serve(8080);curl -F 'title=rapor' -F 'file=@sample.txt' localhost:8080/upload# → 201 {"title":"rapor","files":[{"name":"sample.txt","size":13}]}Statik dosyaları nasıl sunarım?
Section titled “Statik dosyaları nasıl sunarım?”static(prefix, dir) bir dizini 404-fallback olarak mount eder — gerçek
route’lar her zaman kazanır, static catch-all’dur.
import "wings";
func health(req) { return ok({"status": "up"}); }
get("/api/health", "health");static("/static", "./public"); // GET /static/app.css → ./public/app.cssserve(8080);GET /static/index.html → dosya içeriği (text/html)GET /static/app.css → dosya içeriği (text/css)GET /static/missing.js → 404GET /api/health → {"status":"up"}Metin asset’leri doğru Content-Type alır; binary’ler (PNG vb.) byte-exact
round-trip eder. Path traversal (..) reddedilir.
API’mi nasıl sürümlerim?
Section titled “API’mi nasıl sürümlerim?”group(prefix, fn), fn’in kaydettiği her route’a prefix ekler. Gruplar iç
içe geçer, böylece /api/v1 ve /api/v2’yi yan yana mount edebilirsin.
import "wings";
func v1_users(req) { return ok({"version": "v1", "users": []}); }func v2_users(req) { return ok({"version": "v2", "users": []}); }
func api_v1() { get("/users", "v1_users"); }func api_v2() { get("/users", "v2_users"); }
group("/api/v1", "api_v1");group("/api/v2", "api_v2");serve(8080);GET /api/v1/users → {"version":"v1","users":[]}GET /api/v2/users → {"version":"v2","users":[]}Yanıttan gizli alanları nasıl saklarım?
Section titled “Yanıttan gizli alanları nasıl saklarım?”response_model(schema) başarılı çıktıyı bildirilen alanlara filtreler — bir
password_hash veya iç bayrak serileştirmeden önce düşürülür, böylece bir
handler tüm satırını sızdırmadan dönebilir.
import "wings";
func show_user(req) { return ok({"id": 1, "name": "Ada", "email": "ada@x.io", "password_hash": "$2b$...", "_internal": "audit-7"});}
get("/users/:id", "show_user");response_model({"id": "int", "name": "str", "email": "str"});serve(8080);GET /users/1 → {"id":1,"name":"Ada","email":"ada@x.io"} (password_hash + _internal düşürüldü)Hatalar (status ≥ 400) filtrelemeyi atlar, böylece {"error": ...} gövdesi
korunur.
Sıcak bir endpoint’i nasıl cache’lerim?
Section titled “Sıcak bir endpoint’i nasıl cache’lerim?”Path’in saf fonksiyonu olan bir yanıt için (config, sürüm banner’ı),
cached_get ilk istekten sonra her vuruşu pinlenmiş bir wire buffer’dan sunar —
handler dispatch’i ve JSON serileştirmeyi tamamen atlar.
import "wings";
func app_config(req) { return ok({"name": "MyApp", "features": ["a", "b"], "version": "3.1.0"});}
cached_get("/config", "app_config");serve(8080);Yalnızca çıktı isteğe veya duvar-saatine bağlı değilken kullan (cache ilk sonucu dondurur). Henüz invalidation API’si yok — yeniden cache restart’ta olur.
CSV nasıl dönerim (ya da dosya indirmeyi nasıl tetiklerim)?
Section titled “CSV nasıl dönerim (ya da dosya indirmeyi nasıl tetiklerim)?”Özel bir content type için _raw / _content_type envelope’unu dön (düz string
gövde, JSON sarmalama yok):
import "wings";
func export_csv(req) { str body = "id,name\n1,Ada\n2,Linus\n"; return {"_raw": body, "_content_type": "text/csv; charset=utf-8"};}
get("/export.csv", "export_csv");serve(8080);Ayrıca özel header’lara ihtiyacın olduğunda — Content-Disposition indirme
dosya adı gibi — tam yanıtı sokete kendin yaz ve {"_stream": 1} ile sinyal
ver, böylece Wings onu sarmalamaz:
import "wings";
func download_csv(req) { str body = "id,name\n1,Ada\n2,Linus\n"; json headers = {"Content-Disposition": "attachment; filename=\"users.csv\""}; str wire = http_create_response(200, "text/csv; charset=utf-8", body, headers, 0); socket_send(wings_current_fd(), wire); return {"_stream": 1}; // "soketi ben yazdım; yanıt kurma"}
get("/download.csv", "download_csv");serve(8080);API’mi SQLite ile nasıl desteklerim?
Section titled “API’mi SQLite ile nasıl desteklerim?”In-memory global’i ORM ile değiştir: bir kez model
tanımla, sonra orm_create / orm_find / orm_all sana kalıcı satırlar verir.
import "wings";import "orm";
orm_open("./app.db");define_model("users", { "id": "INTEGER PRIMARY KEY AUTOINCREMENT", "name": "TEXT", "email": "TEXT"});
func list_users(req) { return ok(orm_all("users")); }
func create_user(req) { json b = req["json"]; int id = orm_create("users", {"name": b["name"], "email": b["email"]}); return created(orm_find("users", id));}
func get_user(req) { json u = orm_find("users", toInt(req["params"]["id"])); if (length(keys(u)) == 0) { return not_found("kullanıcı yok"); } return ok(u);}
get("/users", "list_users");post("/users", "create_user");get("/users/:id", "get_user");serve(8080);POST /users {"name":"Ada","email":"a@b.io"} → 201 {"id":1,"name":"Ada",...}GET /users → [{"id":1,...}]GET /users/1 → {"id":1,...}Sırada ne var?
Section titled “Sırada ne var?”- HTTP Sunucusu (Wings) — tam referans: serve
modları, request objesi, response helper’ları, OpenAPI,
/docsve TLS. - ORM (lib/orm) — modeller, sorgular ve güncellemeler.
- Paket Yöneticisi — Wings uygulamanın yeniden kullanılabilir parçalarını yayınla.