mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 03:43:30 +08:00
Add Turkish (tr) docs and update README (#744)
* Add Turkish (tr) docs and update README Add a full set of Turkish documentation under docs/tr (agents, changelog, CLAUDE guide, contributing, code of conduct, and many agents/commands/skills/rules files). Update README to include a link to the Turkish docs and increment the supported language count from 5 to 6. This commit adds localized guidance and references to help Turkish-speaking contributors and users. * Update docs/tr/TROUBLESHOOTING.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Update docs/tr/README.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * docs(tr): fix license link and update readmes Update Turkish docs: change license badge link to point to repository root (../../LICENSE), increment displayed language count from 5 to 6, and remove two outdated related links from docs/tr/examples/README.md to keep references accurate. * Update docs/tr/commands/instinct-import.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Update docs/tr/commands/checkpoint.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
bb1efad7c7
commit
fd2a8edb53
523
docs/tr/skills/api-design/SKILL.md
Normal file
523
docs/tr/skills/api-design/SKILL.md
Normal file
@@ -0,0 +1,523 @@
|
||||
---
|
||||
name: api-design
|
||||
description: REST API tasarım kalıpları; kaynak isimlendirme, durum kodları, sayfalama, filtreleme, hata yanıtları, versiyonlama ve üretim API'leri için hız sınırlama içerir.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# API Tasarım Kalıpları
|
||||
|
||||
Tutarlı, geliştirici dostu REST API'leri tasarlamak için konvansiyonlar ve en iyi uygulamalar.
|
||||
|
||||
## Ne Zaman Aktifleştirmeli
|
||||
|
||||
- Yeni API endpoint'leri tasarlarken
|
||||
- Mevcut API sözleşmelerini incelerken
|
||||
- Sayfalama, filtreleme veya sıralama eklerken
|
||||
- API'ler için hata işleme uygularken
|
||||
- API versiyonlama stratejisi planlarken
|
||||
- Halka açık veya iş ortağı odaklı API'ler oluştururken
|
||||
|
||||
## Kaynak Tasarımı
|
||||
|
||||
### URL Yapısı
|
||||
|
||||
```
|
||||
# Kaynaklar isim, çoğul, küçük harf, kebab-case
|
||||
GET /api/v1/users
|
||||
GET /api/v1/users/:id
|
||||
POST /api/v1/users
|
||||
PUT /api/v1/users/:id
|
||||
PATCH /api/v1/users/:id
|
||||
DELETE /api/v1/users/:id
|
||||
|
||||
# İlişkiler için alt kaynaklar
|
||||
GET /api/v1/users/:id/orders
|
||||
POST /api/v1/users/:id/orders
|
||||
|
||||
# CRUD'a uymayan aksiyonlar (fiilleri dikkatli kullanın)
|
||||
POST /api/v1/orders/:id/cancel
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/refresh
|
||||
```
|
||||
|
||||
### İsimlendirme Kuralları
|
||||
|
||||
```
|
||||
# İYİ
|
||||
/api/v1/team-members # çok sözcüklü kaynaklar için kebab-case
|
||||
/api/v1/orders?status=active # filtreleme için query parametreleri
|
||||
/api/v1/users/123/orders # sahiplik için iç içe kaynaklar
|
||||
|
||||
# KÖTÜ
|
||||
/api/v1/getUsers # URL'de fiil
|
||||
/api/v1/user # tekil (çoğul kullanın)
|
||||
/api/v1/team_members # URL'lerde snake_case
|
||||
/api/v1/users/123/getOrders # iç içe kaynaklarda fiil
|
||||
```
|
||||
|
||||
## HTTP Metodları ve Durum Kodları
|
||||
|
||||
### Metod Semantiği
|
||||
|
||||
| Metod | Idempotent | Güvenli | Kullanım Amacı |
|
||||
|--------|-----------|------|---------|
|
||||
| GET | Evet | Evet | Kaynakları getir |
|
||||
| POST | Hayır | Hayır | Kaynak oluştur, aksiyonları tetikle |
|
||||
| PUT | Evet | Hayır | Kaynağın tam değişimi |
|
||||
| PATCH | Hayır* | Hayır | Kaynağın kısmi güncellemesi |
|
||||
| DELETE | Evet | Hayır | Kaynağı kaldır |
|
||||
|
||||
*PATCH uygun implementasyonla idempotent yapılabilir
|
||||
|
||||
### Durum Kodu Referansı
|
||||
|
||||
```
|
||||
# Başarı
|
||||
200 OK — GET, PUT, PATCH (yanıt body'si ile)
|
||||
201 Created — POST (Location header ekleyin)
|
||||
204 No Content — DELETE, PUT (yanıt body'si yok)
|
||||
|
||||
# İstemci Hataları
|
||||
400 Bad Request — Validasyon hatası, hatalı JSON
|
||||
401 Unauthorized — Eksik veya geçersiz kimlik doğrulama
|
||||
403 Forbidden — Kimlik doğrulandı ama yetkilendirilmedi
|
||||
404 Not Found — Kaynak mevcut değil
|
||||
409 Conflict — Tekrar kayıt, durum çakışması
|
||||
422 Unprocessable Entity — Semantik olarak geçersiz (geçerli JSON, kötü veri)
|
||||
429 Too Many Requests — Hız limiti aşıldı
|
||||
|
||||
# Sunucu Hataları
|
||||
500 Internal Server Error — Beklenmeyen hata (detayları açığa çıkarmayın)
|
||||
502 Bad Gateway — Upstream servis başarısız
|
||||
503 Service Unavailable — Geçici aşırı yük, Retry-After ekleyin
|
||||
```
|
||||
|
||||
### Yaygın Hatalar
|
||||
|
||||
```
|
||||
# KÖTÜ: Her şey için 200
|
||||
{ "status": 200, "success": false, "error": "Not found" }
|
||||
|
||||
# İYİ: HTTP durum kodlarını semantik olarak kullanın
|
||||
HTTP/1.1 404 Not Found
|
||||
{ "error": { "code": "not_found", "message": "User not found" } }
|
||||
|
||||
# KÖTÜ: Validasyon hataları için 500
|
||||
# İYİ: Alan düzeyinde detaylarla 400 veya 422
|
||||
|
||||
# KÖTÜ: Oluşturulan kaynaklar için 200
|
||||
# İYİ: Location header ile 201
|
||||
HTTP/1.1 201 Created
|
||||
Location: /api/v1/users/abc-123
|
||||
```
|
||||
|
||||
## Yanıt Formatı
|
||||
|
||||
### Başarı Yanıtı
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "abc-123",
|
||||
"email": "alice@example.com",
|
||||
"name": "Alice",
|
||||
"created_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Koleksiyon Yanıtı (Sayfalama ile)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "id": "abc-123", "name": "Alice" },
|
||||
{ "id": "def-456", "name": "Bob" }
|
||||
],
|
||||
"meta": {
|
||||
"total": 142,
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"total_pages": 8
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/v1/users?page=1&per_page=20",
|
||||
"next": "/api/v1/users?page=2&per_page=20",
|
||||
"last": "/api/v1/users?page=8&per_page=20"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hata Yanıtı
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "validation_error",
|
||||
"message": "Request validation failed",
|
||||
"details": [
|
||||
{
|
||||
"field": "email",
|
||||
"message": "Must be a valid email address",
|
||||
"code": "invalid_format"
|
||||
},
|
||||
{
|
||||
"field": "age",
|
||||
"message": "Must be between 0 and 150",
|
||||
"code": "out_of_range"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Yanıt Zarfı Varyantları
|
||||
|
||||
```typescript
|
||||
// Seçenek A: Data sarmalayıcılı zarf (halka açık API'ler için önerilir)
|
||||
interface ApiResponse<T> {
|
||||
data: T;
|
||||
meta?: PaginationMeta;
|
||||
links?: PaginationLinks;
|
||||
}
|
||||
|
||||
interface ApiError {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: FieldError[];
|
||||
};
|
||||
}
|
||||
|
||||
// Seçenek B: Düz yanıt (daha basit, dahili API'ler için yaygın)
|
||||
// Başarı: kaynağı doğrudan döndür
|
||||
// Hata: hata nesnesini döndür
|
||||
// HTTP durum koduyla ayırt et
|
||||
```
|
||||
|
||||
## Sayfalama
|
||||
|
||||
### Offset-Tabanlı (Basit)
|
||||
|
||||
```
|
||||
GET /api/v1/users?page=2&per_page=20
|
||||
|
||||
# Implementasyon
|
||||
SELECT * FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20 OFFSET 20;
|
||||
```
|
||||
|
||||
**Artıları:** Uygulaması kolay, "N sayfasına git" destekler
|
||||
**Eksileri:** Büyük offset'lerde yavaş (OFFSET 100000), eş zamanlı eklemelerde tutarsız
|
||||
|
||||
### Cursor-Tabanlı (Ölçeklenebilir)
|
||||
|
||||
```
|
||||
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
|
||||
|
||||
# Implementasyon
|
||||
SELECT * FROM users
|
||||
WHERE id > :cursor_id
|
||||
ORDER BY id ASC
|
||||
LIMIT 21; -- has_next belirlemek için bir fazla getir
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"meta": {
|
||||
"has_next": true,
|
||||
"next_cursor": "eyJpZCI6MTQzfQ"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Artıları:** Pozisyondan bağımsız tutarlı performans, eş zamanlı eklemelerde kararlı
|
||||
**Eksileri:** Rastgele sayfaya atlayamaz, cursor opak
|
||||
|
||||
### Hangisi Ne Zaman Kullanılmalı
|
||||
|
||||
| Kullanım Senaryosu | Sayfalama Tipi |
|
||||
|----------|----------------|
|
||||
| Admin panelleri, küçük veri setleri (<10K) | Offset |
|
||||
| Sonsuz kaydırma, akışlar, büyük veri setleri | Cursor |
|
||||
| Halka açık API'ler | Cursor (varsayılan) ile offset (opsiyonel) |
|
||||
| Arama sonuçları | Offset (kullanıcılar sayfa numarası bekler) |
|
||||
|
||||
## Filtreleme, Sıralama ve Arama
|
||||
|
||||
### Filtreleme
|
||||
|
||||
```
|
||||
# Basit eşitlik
|
||||
GET /api/v1/orders?status=active&customer_id=abc-123
|
||||
|
||||
# Karşılaştırma operatörleri (köşeli parantez notasyonu kullanın)
|
||||
GET /api/v1/products?price[gte]=10&price[lte]=100
|
||||
GET /api/v1/orders?created_at[after]=2025-01-01
|
||||
|
||||
# Çoklu değerler (virgülle ayrılmış)
|
||||
GET /api/v1/products?category=electronics,clothing
|
||||
|
||||
# İç içe alanlar (nokta notasyonu)
|
||||
GET /api/v1/orders?customer.country=US
|
||||
```
|
||||
|
||||
### Sıralama
|
||||
|
||||
```
|
||||
# Tek alan (azalan için - öneki)
|
||||
GET /api/v1/products?sort=-created_at
|
||||
|
||||
# Çoklu alanlar (virgülle ayrılmış)
|
||||
GET /api/v1/products?sort=-featured,price,-created_at
|
||||
```
|
||||
|
||||
### Tam Metin Arama
|
||||
|
||||
```
|
||||
# Arama query parametresi
|
||||
GET /api/v1/products?q=wireless+headphones
|
||||
|
||||
# Alana özel arama
|
||||
GET /api/v1/users?email=alice
|
||||
```
|
||||
|
||||
### Seyrek Fieldset'ler
|
||||
|
||||
```
|
||||
# Sadece belirtilen alanları döndür (payload'ı azaltır)
|
||||
GET /api/v1/users?fields=id,name,email
|
||||
GET /api/v1/orders?fields=id,total,status&include=customer.name
|
||||
```
|
||||
|
||||
## Kimlik Doğrulama ve Yetkilendirme
|
||||
|
||||
### Token-Tabanlı Auth
|
||||
|
||||
```
|
||||
# Authorization header'da Bearer token
|
||||
GET /api/v1/users
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
|
||||
# API key (sunucudan sunucuya)
|
||||
GET /api/v1/data
|
||||
X-API-Key: sk_live_abc123
|
||||
```
|
||||
|
||||
### Yetkilendirme Kalıpları
|
||||
|
||||
```typescript
|
||||
// Kaynak seviyesi: sahipliği kontrol et
|
||||
app.get("/api/v1/orders/:id", async (req, res) => {
|
||||
const order = await Order.findById(req.params.id);
|
||||
if (!order) return res.status(404).json({ error: { code: "not_found" } });
|
||||
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
|
||||
return res.json({ data: order });
|
||||
});
|
||||
|
||||
// Rol-tabanlı: yetkileri kontrol et
|
||||
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
|
||||
await User.delete(req.params.id);
|
||||
return res.status(204).send();
|
||||
});
|
||||
```
|
||||
|
||||
## Hız Sınırlama
|
||||
|
||||
### Header'lar
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1640000000
|
||||
|
||||
# Aşıldığında
|
||||
HTTP/1.1 429 Too Many Requests
|
||||
Retry-After: 60
|
||||
{
|
||||
"error": {
|
||||
"code": "rate_limit_exceeded",
|
||||
"message": "Rate limit exceeded. Try again in 60 seconds."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hız Limit Katmanları
|
||||
|
||||
| Katman | Limit | Pencere | Kullanım Senaryosu |
|
||||
|------|-------|--------|----------|
|
||||
| Anonim | 30/dk | IP Başına | Halka açık endpoint'ler |
|
||||
| Kimlik Doğrulanmış | 100/dk | Kullanıcı Başına | Standart API erişimi |
|
||||
| Premium | 1000/dk | API key Başına | Ücretli API planları |
|
||||
| Dahili | 10000/dk | Servis Başına | Servisten servise |
|
||||
|
||||
## Versiyonlama
|
||||
|
||||
### URL Yolu Versiyonlama (Önerilen)
|
||||
|
||||
```
|
||||
/api/v1/users
|
||||
/api/v2/users
|
||||
```
|
||||
|
||||
**Artıları:** Açık, yönlendirmesi kolay, cache'lenebilir
|
||||
**Eksileri:** Versiyonlar arası URL değişir
|
||||
|
||||
### Header Versiyonlama
|
||||
|
||||
```
|
||||
GET /api/users
|
||||
Accept: application/vnd.myapp.v2+json
|
||||
```
|
||||
|
||||
**Artıları:** Temiz URL'ler
|
||||
**Eksileri:** Test etmesi zor, unutulması kolay
|
||||
|
||||
### Versiyonlama Stratejisi
|
||||
|
||||
```
|
||||
1. /api/v1/ ile başlayın — ihtiyaç duyana kadar versiyonlamayın
|
||||
2. En fazla 2 aktif versiyon koruyun (mevcut + önceki)
|
||||
3. Kullanımdan kaldırma zaman çizelgesi:
|
||||
- Kullanımdan kaldırmayı duyurun (halka açık API'ler için 6 ay önceden)
|
||||
- Sunset header ekleyin: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
|
||||
- Sunset tarihinden sonra 410 Gone döndürün
|
||||
4. Breaking olmayan değişiklikler yeni versiyon gerektirmez:
|
||||
- Yanıtlara yeni alanlar eklemek
|
||||
- Yeni opsiyonel query parametreleri eklemek
|
||||
- Yeni endpoint'ler eklemek
|
||||
5. Breaking değişiklikler yeni versiyon gerektirir:
|
||||
- Alanları kaldırmak veya yeniden adlandırmak
|
||||
- Alan tiplerini değiştirmek
|
||||
- URL yapısını değiştirmek
|
||||
- Kimlik doğrulama metodunu değiştirmek
|
||||
```
|
||||
|
||||
## Implementasyon Kalıpları
|
||||
|
||||
### TypeScript (Next.js API Route)
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const parsed = createUserSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({
|
||||
error: {
|
||||
code: "validation_error",
|
||||
message: "Request validation failed",
|
||||
details: parsed.error.issues.map(i => ({
|
||||
field: i.path.join("."),
|
||||
message: i.message,
|
||||
code: i.code,
|
||||
})),
|
||||
},
|
||||
}, { status: 422 });
|
||||
}
|
||||
|
||||
const user = await createUser(parsed.data);
|
||||
|
||||
return NextResponse.json(
|
||||
{ data: user },
|
||||
{
|
||||
status: 201,
|
||||
headers: { Location: `/api/v1/users/${user.id}` },
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Python (Django REST Framework)
|
||||
|
||||
```python
|
||||
from rest_framework import serializers, viewsets, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
class CreateUserSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
name = serializers.CharField(max_length=100)
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "email", "name", "created_at"]
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return CreateUserSerializer
|
||||
return UserSerializer
|
||||
|
||||
def create(self, request):
|
||||
serializer = CreateUserSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = UserService.create(**serializer.validated_data)
|
||||
return Response(
|
||||
{"data": UserSerializer(user).data},
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers={"Location": f"/api/v1/users/{user.id}"},
|
||||
)
|
||||
```
|
||||
|
||||
### Go (net/http)
|
||||
|
||||
```go
|
||||
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.service.Create(r.Context(), req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrEmailTaken):
|
||||
writeError(w, http.StatusConflict, "email_taken", "Email already registered")
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"data": user})
|
||||
}
|
||||
```
|
||||
|
||||
## API Tasarım Kontrol Listesi
|
||||
|
||||
Yeni bir endpoint yayınlamadan önce:
|
||||
|
||||
- [ ] Kaynak URL isimlendirme konvansiyonlarını takip ediyor (çoğul, kebab-case, fiil yok)
|
||||
- [ ] Doğru HTTP metodu kullanılıyor (okumalar için GET, oluşturmalar için POST, vb.)
|
||||
- [ ] Uygun durum kodları döndürülüyor (her şey için 200 değil)
|
||||
- [ ] Girdi şema ile validasyona tabi tutuluyor (Zod, Pydantic, Bean Validation)
|
||||
- [ ] Hata yanıtları kodlar ve mesajlarla standart formatı takip ediyor
|
||||
- [ ] Liste endpoint'leri için sayfalama uygulanmış (cursor veya offset)
|
||||
- [ ] Kimlik doğrulama gerekli (veya açıkça halka açık işaretlenmiş)
|
||||
- [ ] Yetkilendirme kontrol ediliyor (kullanıcı sadece kendi kaynaklarına erişebilir)
|
||||
- [ ] Hız sınırlama yapılandırılmış
|
||||
- [ ] Yanıt dahili detayları sızdırmıyor (stack trace'ler, SQL hataları)
|
||||
- [ ] Mevcut endpoint'lerle tutarlı isimlendirme (camelCase vs snake_case)
|
||||
- [ ] Dokümante edilmiş (OpenAPI/Swagger spec güncellenmiş)
|
||||
598
docs/tr/skills/backend-patterns/SKILL.md
Normal file
598
docs/tr/skills/backend-patterns/SKILL.md
Normal file
@@ -0,0 +1,598 @@
|
||||
---
|
||||
name: backend-patterns
|
||||
description: Node.js, Express ve Next.js API routes için backend mimari kalıpları, API tasarımı, veritabanı optimizasyonu ve sunucu tarafı en iyi uygulamalar.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Backend Geliştirme Kalıpları
|
||||
|
||||
Ölçeklenebilir sunucu tarafı uygulamalar için backend mimari kalıpları ve en iyi uygulamalar.
|
||||
|
||||
## Ne Zaman Aktifleştirmelisiniz
|
||||
|
||||
- REST veya GraphQL API endpoint'leri tasarlarken
|
||||
- Repository, service veya controller katmanları uygularken
|
||||
- Veritabanı sorgularını optimize ederken (N+1, indeksleme, bağlantı havuzu)
|
||||
- Önbellekleme eklerken (Redis, in-memory, HTTP cache başlıkları)
|
||||
- Arka plan işleri veya async işleme ayarlarken
|
||||
- API'ler için hata yönetimi ve doğrulama yapılandırırken
|
||||
- Middleware oluştururken (auth, logging, rate limiting)
|
||||
|
||||
## API Tasarım Kalıpları
|
||||
|
||||
### RESTful API Yapısı
|
||||
|
||||
```typescript
|
||||
// ✅ Kaynak tabanlı URL'ler
|
||||
GET /api/markets # Kaynakları listele
|
||||
GET /api/markets/:id # Tek kaynak getir
|
||||
POST /api/markets # Kaynak oluştur
|
||||
PUT /api/markets/:id # Kaynağı değiştir (tam)
|
||||
PATCH /api/markets/:id # Kaynağı güncelle (kısmi)
|
||||
DELETE /api/markets/:id # Kaynağı sil
|
||||
|
||||
// ✅ Filtreleme, sıralama, sayfalama için query parametreleri
|
||||
GET /api/markets?status=active&sort=volume&limit=20&offset=0
|
||||
```
|
||||
|
||||
### Repository Kalıbı
|
||||
|
||||
```typescript
|
||||
// Veri erişim mantığını soyutla
|
||||
interface MarketRepository {
|
||||
findAll(filters?: MarketFilters): Promise<Market[]>
|
||||
findById(id: string): Promise<Market | null>
|
||||
create(data: CreateMarketDto): Promise<Market>
|
||||
update(id: string, data: UpdateMarketDto): Promise<Market>
|
||||
delete(id: string): Promise<void>
|
||||
}
|
||||
|
||||
class SupabaseMarketRepository implements MarketRepository {
|
||||
async findAll(filters?: MarketFilters): Promise<Market[]> {
|
||||
let query = supabase.from('markets').select('*')
|
||||
|
||||
if (filters?.status) {
|
||||
query = query.eq('status', filters.status)
|
||||
}
|
||||
|
||||
if (filters?.limit) {
|
||||
query = query.limit(filters.limit)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) throw new Error(error.message)
|
||||
return data
|
||||
}
|
||||
|
||||
// Diğer metodlar...
|
||||
}
|
||||
```
|
||||
|
||||
### Service Katmanı Kalıbı
|
||||
|
||||
```typescript
|
||||
// İş mantığı veri erişiminden ayrılmış
|
||||
class MarketService {
|
||||
constructor(private marketRepo: MarketRepository) {}
|
||||
|
||||
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
|
||||
// İş mantığı
|
||||
const embedding = await generateEmbedding(query)
|
||||
const results = await this.vectorSearch(embedding, limit)
|
||||
|
||||
// Tam veriyi getir
|
||||
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
|
||||
|
||||
// Benzerliğe göre sırala
|
||||
return markets.sort((a, b) => {
|
||||
const scoreA = results.find(r => r.id === a.id)?.score || 0
|
||||
const scoreB = results.find(r => r.id === b.id)?.score || 0
|
||||
return scoreA - scoreB
|
||||
})
|
||||
}
|
||||
|
||||
private async vectorSearch(embedding: number[], limit: number) {
|
||||
// Vector arama implementasyonu
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Kalıbı
|
||||
|
||||
```typescript
|
||||
// Request/response işleme hattı
|
||||
export function withAuth(handler: NextApiHandler): NextApiHandler {
|
||||
return async (req, res) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Unauthorized' })
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await verifyToken(token)
|
||||
req.user = user
|
||||
return handler(req, res)
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Invalid token' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
export default withAuth(async (req, res) => {
|
||||
// Handler req.user'a erişebilir
|
||||
})
|
||||
```
|
||||
|
||||
## Veritabanı Kalıpları
|
||||
|
||||
### Sorgu Optimizasyonu
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Sadece gerekli sütunları seç
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('id, name, status, volume')
|
||||
.eq('status', 'active')
|
||||
.order('volume', { ascending: false })
|
||||
.limit(10)
|
||||
|
||||
// ❌ KÖTÜ: Her şeyi seç
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('*')
|
||||
```
|
||||
|
||||
### N+1 Sorgu Önleme
|
||||
|
||||
```typescript
|
||||
// ❌ KÖTÜ: N+1 sorgu problemi
|
||||
const markets = await getMarkets()
|
||||
for (const market of markets) {
|
||||
market.creator = await getUser(market.creator_id) // N sorgu
|
||||
}
|
||||
|
||||
// ✅ İYİ: Toplu getirme
|
||||
const markets = await getMarkets()
|
||||
const creatorIds = markets.map(m => m.creator_id)
|
||||
const creators = await getUsers(creatorIds) // 1 sorgu
|
||||
const creatorMap = new Map(creators.map(c => [c.id, c]))
|
||||
|
||||
markets.forEach(market => {
|
||||
market.creator = creatorMap.get(market.creator_id)
|
||||
})
|
||||
```
|
||||
|
||||
### Transaction Kalıbı
|
||||
|
||||
```typescript
|
||||
async function createMarketWithPosition(
|
||||
marketData: CreateMarketDto,
|
||||
positionData: CreatePositionDto
|
||||
) {
|
||||
// Supabase transaction kullan
|
||||
const { data, error } = await supabase.rpc('create_market_with_position', {
|
||||
market_data: marketData,
|
||||
position_data: positionData
|
||||
})
|
||||
|
||||
if (error) throw new Error('Transaction failed')
|
||||
return data
|
||||
}
|
||||
|
||||
// Supabase'de SQL fonksiyonu
|
||||
CREATE OR REPLACE FUNCTION create_market_with_position(
|
||||
market_data jsonb,
|
||||
position_data jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Transaction otomatik başlar
|
||||
INSERT INTO markets VALUES (market_data);
|
||||
INSERT INTO positions VALUES (position_data);
|
||||
RETURN jsonb_build_object('success', true);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Rollback otomatik olur
|
||||
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Önbellekleme Stratejileri
|
||||
|
||||
### Redis Önbellekleme Katmanı
|
||||
|
||||
```typescript
|
||||
class CachedMarketRepository implements MarketRepository {
|
||||
constructor(
|
||||
private baseRepo: MarketRepository,
|
||||
private redis: RedisClient
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<Market | null> {
|
||||
// Önce önbelleği kontrol et
|
||||
const cached = await this.redis.get(`market:${id}`)
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached)
|
||||
}
|
||||
|
||||
// Cache miss - veritabanından getir
|
||||
const market = await this.baseRepo.findById(id)
|
||||
|
||||
if (market) {
|
||||
// 5 dakika önbellekle
|
||||
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
|
||||
}
|
||||
|
||||
return market
|
||||
}
|
||||
|
||||
async invalidateCache(id: string): Promise<void> {
|
||||
await this.redis.del(`market:${id}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache-Aside Kalıbı
|
||||
|
||||
```typescript
|
||||
async function getMarketWithCache(id: string): Promise<Market> {
|
||||
const cacheKey = `market:${id}`
|
||||
|
||||
// Önbelleği dene
|
||||
const cached = await redis.get(cacheKey)
|
||||
if (cached) return JSON.parse(cached)
|
||||
|
||||
// Cache miss - DB'den getir
|
||||
const market = await db.markets.findUnique({ where: { id } })
|
||||
|
||||
if (!market) throw new Error('Market not found')
|
||||
|
||||
// Önbelleği güncelle
|
||||
await redis.setex(cacheKey, 300, JSON.stringify(market))
|
||||
|
||||
return market
|
||||
}
|
||||
```
|
||||
|
||||
## Hata Yönetimi Kalıpları
|
||||
|
||||
### Merkezi Hata Yöneticisi
|
||||
|
||||
```typescript
|
||||
class ApiError extends Error {
|
||||
constructor(
|
||||
public statusCode: number,
|
||||
public message: string,
|
||||
public isOperational = true
|
||||
) {
|
||||
super(message)
|
||||
Object.setPrototypeOf(this, ApiError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export function errorHandler(error: unknown, req: Request): Response {
|
||||
if (error instanceof ApiError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: error.statusCode })
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.errors
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Beklenmeyen hataları logla
|
||||
console.error('Unexpected error:', error)
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}, { status: 500 })
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const data = await fetchData()
|
||||
return NextResponse.json({ success: true, data })
|
||||
} catch (error) {
|
||||
return errorHandler(error, request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Exponential Backoff ile Tekrar Deneme
|
||||
|
||||
```typescript
|
||||
async function fetchWithRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries = 3
|
||||
): Promise<T> {
|
||||
let lastError: Error
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delay = Math.pow(2, i) * 1000
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
const data = await fetchWithRetry(() => fetchFromAPI())
|
||||
```
|
||||
|
||||
## Kimlik Doğrulama ve Yetkilendirme
|
||||
|
||||
### JWT Token Doğrulama
|
||||
|
||||
```typescript
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string
|
||||
email: string
|
||||
role: 'admin' | 'user'
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): JWTPayload {
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
|
||||
return payload
|
||||
} catch (error) {
|
||||
throw new ApiError(401, 'Invalid token')
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireAuth(request: Request) {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
throw new ApiError(401, 'Missing authorization token')
|
||||
}
|
||||
|
||||
return verifyToken(token)
|
||||
}
|
||||
|
||||
// API route'unda kullanım
|
||||
export async function GET(request: Request) {
|
||||
const user = await requireAuth(request)
|
||||
|
||||
const data = await getDataForUser(user.userId)
|
||||
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
```
|
||||
|
||||
### Rol Tabanlı Erişim Kontrolü
|
||||
|
||||
```typescript
|
||||
type Permission = 'read' | 'write' | 'delete' | 'admin'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
role: 'admin' | 'moderator' | 'user'
|
||||
}
|
||||
|
||||
const rolePermissions: Record<User['role'], Permission[]> = {
|
||||
admin: ['read', 'write', 'delete', 'admin'],
|
||||
moderator: ['read', 'write', 'delete'],
|
||||
user: ['read', 'write']
|
||||
}
|
||||
|
||||
export function hasPermission(user: User, permission: Permission): boolean {
|
||||
return rolePermissions[user.role].includes(permission)
|
||||
}
|
||||
|
||||
export function requirePermission(permission: Permission) {
|
||||
return (handler: (request: Request, user: User) => Promise<Response>) => {
|
||||
return async (request: Request) => {
|
||||
const user = await requireAuth(request)
|
||||
|
||||
if (!hasPermission(user, permission)) {
|
||||
throw new ApiError(403, 'Insufficient permissions')
|
||||
}
|
||||
|
||||
return handler(request, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kullanım - HOF handler'ı sarar
|
||||
export const DELETE = requirePermission('delete')(
|
||||
async (request: Request, user: User) => {
|
||||
// Handler doğrulanmış yetki ile kullanıcı alır
|
||||
return new Response('Deleted', { status: 200 })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Basit In-Memory Rate Limiter
|
||||
|
||||
```typescript
|
||||
class RateLimiter {
|
||||
private requests = new Map<string, number[]>()
|
||||
|
||||
async checkLimit(
|
||||
identifier: string,
|
||||
maxRequests: number,
|
||||
windowMs: number
|
||||
): Promise<boolean> {
|
||||
const now = Date.now()
|
||||
const requests = this.requests.get(identifier) || []
|
||||
|
||||
// Pencere dışındaki eski istekleri kaldır
|
||||
const recentRequests = requests.filter(time => now - time < windowMs)
|
||||
|
||||
if (recentRequests.length >= maxRequests) {
|
||||
return false // Rate limit aşıldı
|
||||
}
|
||||
|
||||
// Mevcut isteği ekle
|
||||
recentRequests.push(now)
|
||||
this.requests.set(identifier, recentRequests)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const limiter = new RateLimiter()
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
||||
|
||||
const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/dak
|
||||
|
||||
if (!allowed) {
|
||||
return NextResponse.json({
|
||||
error: 'Rate limit exceeded'
|
||||
}, { status: 429 })
|
||||
}
|
||||
|
||||
// İstekle devam et
|
||||
}
|
||||
```
|
||||
|
||||
## Arka Plan İşleri ve Kuyruklar
|
||||
|
||||
### Basit Kuyruk Kalıbı
|
||||
|
||||
```typescript
|
||||
class JobQueue<T> {
|
||||
private queue: T[] = []
|
||||
private processing = false
|
||||
|
||||
async add(job: T): Promise<void> {
|
||||
this.queue.push(job)
|
||||
|
||||
if (!this.processing) {
|
||||
this.process()
|
||||
}
|
||||
}
|
||||
|
||||
private async process(): Promise<void> {
|
||||
this.processing = true
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const job = this.queue.shift()!
|
||||
|
||||
try {
|
||||
await this.execute(job)
|
||||
} catch (error) {
|
||||
console.error('Job failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.processing = false
|
||||
}
|
||||
|
||||
private async execute(job: T): Promise<void> {
|
||||
// İş yürütme mantığı
|
||||
}
|
||||
}
|
||||
|
||||
// Market indeksleme için kullanım
|
||||
interface IndexJob {
|
||||
marketId: string
|
||||
}
|
||||
|
||||
const indexQueue = new JobQueue<IndexJob>()
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { marketId } = await request.json()
|
||||
|
||||
// Bloke etmek yerine kuyruğa ekle
|
||||
await indexQueue.add({ marketId })
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Job queued' })
|
||||
}
|
||||
```
|
||||
|
||||
## Loglama ve İzleme
|
||||
|
||||
### Yapılandırılmış Loglama
|
||||
|
||||
```typescript
|
||||
interface LogContext {
|
||||
userId?: string
|
||||
requestId?: string
|
||||
method?: string
|
||||
path?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
class Logger {
|
||||
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
...context
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(entry))
|
||||
}
|
||||
|
||||
info(message: string, context?: LogContext) {
|
||||
this.log('info', message, context)
|
||||
}
|
||||
|
||||
warn(message: string, context?: LogContext) {
|
||||
this.log('warn', message, context)
|
||||
}
|
||||
|
||||
error(message: string, error: Error, context?: LogContext) {
|
||||
this.log('error', message, {
|
||||
...context,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger()
|
||||
|
||||
// Kullanım
|
||||
export async function GET(request: Request) {
|
||||
const requestId = crypto.randomUUID()
|
||||
|
||||
logger.info('Fetching markets', {
|
||||
requestId,
|
||||
method: 'GET',
|
||||
path: '/api/markets'
|
||||
})
|
||||
|
||||
try {
|
||||
const markets = await fetchMarkets()
|
||||
return NextResponse.json({ success: true, data: markets })
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch markets', error as Error, { requestId })
|
||||
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Unutmayın**: Backend kalıpları ölçeklenebilir, sürdürülebilir sunucu tarafı uygulamalar sağlar. Karmaşıklık seviyenize uyan kalıpları seçin.
|
||||
530
docs/tr/skills/coding-standards/SKILL.md
Normal file
530
docs/tr/skills/coding-standards/SKILL.md
Normal file
@@ -0,0 +1,530 @@
|
||||
---
|
||||
name: coding-standards
|
||||
description: TypeScript, JavaScript, React ve Node.js geliştirme için evrensel kodlama standartları, en iyi uygulamalar ve kalıplar.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Kodlama Standartları ve En İyi Uygulamalar
|
||||
|
||||
Tüm projelerde uygulanabilir evrensel kodlama standartları.
|
||||
|
||||
## Ne Zaman Aktifleştirmelisiniz
|
||||
|
||||
- Yeni bir proje veya modül başlatırken
|
||||
- Kod kalitesi ve sürdürülebilirlik için kod incelerken
|
||||
- Mevcut kodu kurallara uygun hale getirmek için refactor ederken
|
||||
- İsimlendirme, biçimlendirme veya yapısal tutarlılığı zorunlu kılarken
|
||||
- Linting, biçimlendirme veya tür kontrolü kuralları ayarlarken
|
||||
- Yeni katkıda bulunanları kodlama kurallarına alıştırırken
|
||||
|
||||
## Kod Kalitesi İlkeleri
|
||||
|
||||
### 1. Önce Okunabilirlik
|
||||
- Kod yazılmaktan çok okunur
|
||||
- Net değişken ve fonksiyon isimleri
|
||||
- Yorumlardan çok kendi kendini belgeleyen kod tercih edilir
|
||||
- Tutarlı biçimlendirme
|
||||
|
||||
### 2. KISS (Keep It Simple, Stupid - Basit Tut)
|
||||
- Çalışan en basit çözüm
|
||||
- Aşırı mühendislikten kaçının
|
||||
- Erken optimizasyon yapmayın
|
||||
- Anlaşılır kod > akıllıca kod
|
||||
|
||||
### 3. DRY (Don't Repeat Yourself - Kendini Tekrar Etme)
|
||||
- Ortak mantığı fonksiyonlara çıkarın
|
||||
- Yeniden kullanılabilir bileşenler oluşturun
|
||||
- Yardımcı araçları modüller arasında paylaşın
|
||||
- Kopyala-yapıştır programlamadan kaçının
|
||||
|
||||
### 4. YAGNI (You Aren't Gonna Need It - İhtiyacın Olmayacak)
|
||||
- İhtiyaç duyulmadan özellikler oluşturmayın
|
||||
- Spekülatif genellemeden kaçının
|
||||
- Karmaşıklığı sadece gerektiğinde ekleyin
|
||||
- Basit başlayın, gerektiğinde refactor edin
|
||||
|
||||
## TypeScript/JavaScript Standartları
|
||||
|
||||
### Değişken İsimlendirme
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Açıklayıcı isimler
|
||||
const marketSearchQuery = 'election'
|
||||
const isUserAuthenticated = true
|
||||
const totalRevenue = 1000
|
||||
|
||||
// ❌ KÖTÜ: Belirsiz isimler
|
||||
const q = 'election'
|
||||
const flag = true
|
||||
const x = 1000
|
||||
```
|
||||
|
||||
### Fonksiyon İsimlendirme
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Fiil-isim kalıbı
|
||||
async function fetchMarketData(marketId: string) { }
|
||||
function calculateSimilarity(a: number[], b: number[]) { }
|
||||
function isValidEmail(email: string): boolean { }
|
||||
|
||||
// ❌ KÖTÜ: Belirsiz veya sadece isim
|
||||
async function market(id: string) { }
|
||||
function similarity(a, b) { }
|
||||
function email(e) { }
|
||||
```
|
||||
|
||||
### Değişmezlik Kalıbı (KRİTİK)
|
||||
|
||||
```typescript
|
||||
// ✅ HER ZAMAN spread operatörü kullanın
|
||||
const updatedUser = {
|
||||
...user,
|
||||
name: 'New Name'
|
||||
}
|
||||
|
||||
const updatedArray = [...items, newItem]
|
||||
|
||||
// ❌ ASLA doğrudan mutasyon yapmayın
|
||||
user.name = 'New Name' // KÖTÜ
|
||||
items.push(newItem) // KÖTÜ
|
||||
```
|
||||
|
||||
### Hata Yönetimi
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Kapsamlı hata yönetimi
|
||||
async function fetchData(url: string) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Fetch failed:', error)
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ KÖTÜ: Hata yönetimi yok
|
||||
async function fetchData(url) {
|
||||
const response = await fetch(url)
|
||||
return response.json()
|
||||
}
|
||||
```
|
||||
|
||||
### Async/Await En İyi Uygulamaları
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Mümkün olduğunda paralel yürütme
|
||||
const [users, markets, stats] = await Promise.all([
|
||||
fetchUsers(),
|
||||
fetchMarkets(),
|
||||
fetchStats()
|
||||
])
|
||||
|
||||
// ❌ KÖTÜ: Gereksiz yere sıralı
|
||||
const users = await fetchUsers()
|
||||
const markets = await fetchMarkets()
|
||||
const stats = await fetchStats()
|
||||
```
|
||||
|
||||
### Tür Güvenliği
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Doğru tipler
|
||||
interface Market {
|
||||
id: string
|
||||
name: string
|
||||
status: 'active' | 'resolved' | 'closed'
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
function getMarket(id: string): Promise<Market> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// ❌ KÖTÜ: 'any' kullanımı
|
||||
function getMarket(id: any): Promise<any> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## React En İyi Uygulamaları
|
||||
|
||||
### Bileşen Yapısı
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Tiplerle fonksiyonel bileşen
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
variant?: 'primary' | 'secondary'
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
variant = 'primary'
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`btn btn-${variant}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ❌ KÖTÜ: Tip yok, belirsiz yapı
|
||||
export function Button(props) {
|
||||
return <button onClick={props.onClick}>{props.children}</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Özel Hook'lar
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Yeniden kullanılabilir özel hook
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
const debouncedQuery = useDebounce(searchQuery, 500)
|
||||
```
|
||||
|
||||
### State Yönetimi
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Doğru state güncellemeleri
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
// Önceki state'e dayalı fonksiyonel güncelleme
|
||||
setCount(prev => prev + 1)
|
||||
|
||||
// ❌ KÖTÜ: Doğrudan state referansı
|
||||
setCount(count + 1) // Async senaryolarda eski olabilir
|
||||
```
|
||||
|
||||
### Koşullu Render
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Açık koşullu render
|
||||
{isLoading && <Spinner />}
|
||||
{error && <ErrorMessage error={error} />}
|
||||
{data && <DataDisplay data={data} />}
|
||||
|
||||
// ❌ KÖTÜ: Ternary cehennemi
|
||||
{isLoading ? <Spinner /> : error ? <ErrorMessage error={error} /> : data ? <DataDisplay data={data} /> : null}
|
||||
```
|
||||
|
||||
## API Tasarım Standartları
|
||||
|
||||
### REST API Kuralları
|
||||
|
||||
```
|
||||
GET /api/markets # Tüm marketleri listele
|
||||
GET /api/markets/:id # Belirli marketi getir
|
||||
POST /api/markets # Yeni market oluştur
|
||||
PUT /api/markets/:id # Marketi güncelle (tam)
|
||||
PATCH /api/markets/:id # Marketi güncelle (kısmi)
|
||||
DELETE /api/markets/:id # Marketi sil
|
||||
|
||||
# Filtreleme için query parametreleri
|
||||
GET /api/markets?status=active&limit=10&offset=0
|
||||
```
|
||||
|
||||
### Response Formatı
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Tutarlı response yapısı
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
meta?: {
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
// Başarılı response
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: markets,
|
||||
meta: { total: 100, page: 1, limit: 10 }
|
||||
})
|
||||
|
||||
// Hata response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Invalid request'
|
||||
}, { status: 400 })
|
||||
```
|
||||
|
||||
### Input Doğrulama
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// ✅ İYİ: Schema doğrulama
|
||||
const CreateMarketSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().min(1).max(2000),
|
||||
endDate: z.string().datetime(),
|
||||
categories: z.array(z.string()).min(1)
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json()
|
||||
|
||||
try {
|
||||
const validated = CreateMarketSchema.parse(body)
|
||||
// Doğrulanmış veriyle devam et
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.errors
|
||||
}, { status: 400 })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dosya Organizasyonu
|
||||
|
||||
### Proje Yapısı
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # API routes
|
||||
│ ├── markets/ # Market sayfaları
|
||||
│ └── (auth)/ # Auth sayfaları (route groups)
|
||||
├── components/ # React bileşenleri
|
||||
│ ├── ui/ # Genel UI bileşenleri
|
||||
│ ├── forms/ # Form bileşenleri
|
||||
│ └── layouts/ # Layout bileşenleri
|
||||
├── hooks/ # Özel React hooks
|
||||
├── lib/ # Yardımcı araçlar ve konfigürasyonlar
|
||||
│ ├── api/ # API istemcileri
|
||||
│ ├── utils/ # Yardımcı fonksiyonlar
|
||||
│ └── constants/ # Sabitler
|
||||
├── types/ # TypeScript tipleri
|
||||
└── styles/ # Global stiller
|
||||
```
|
||||
|
||||
### Dosya İsimlendirme
|
||||
|
||||
```
|
||||
components/Button.tsx # Bileşenler için PascalCase
|
||||
hooks/useAuth.ts # 'use' öneki ile camelCase
|
||||
lib/formatDate.ts # Yardımcı araçlar için camelCase
|
||||
types/market.types.ts # .types soneki ile camelCase
|
||||
```
|
||||
|
||||
## Yorumlar ve Dokümantasyon
|
||||
|
||||
### Ne Zaman Yorum Yapmalı
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: NİÇİN'i açıklayın, NE'yi değil
|
||||
// Kesintiler sırasında API'yi aşırı yüklemekten kaçınmak için exponential backoff kullan
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000)
|
||||
|
||||
// Büyük dizilerle performans için burada kasıtlı olarak mutasyon kullanılıyor
|
||||
items.push(newItem)
|
||||
|
||||
// ❌ KÖTÜ: Açık olanı belirtmek
|
||||
// Sayacı 1 artır
|
||||
count++
|
||||
|
||||
// İsmi kullanıcının ismine ayarla
|
||||
name = user.name
|
||||
```
|
||||
|
||||
### Public API'ler için JSDoc
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Semantik benzerlik kullanarak market arar.
|
||||
*
|
||||
* @param query - Doğal dil arama sorgusu
|
||||
* @param limit - Maksimum sonuç sayısı (varsayılan: 10)
|
||||
* @returns Benzerlik skoruna göre sıralanmış market dizisi
|
||||
* @throws {Error} OpenAI API başarısız olursa veya Redis kullanılamazsa
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const results = await searchMarkets('election', 5)
|
||||
* console.log(results[0].name) // "Trump vs Biden"
|
||||
* ```
|
||||
*/
|
||||
export async function searchMarkets(
|
||||
query: string,
|
||||
limit: number = 10
|
||||
): Promise<Market[]> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Performans En İyi Uygulamaları
|
||||
|
||||
### Memoization
|
||||
|
||||
```typescript
|
||||
import { useMemo, useCallback } from 'react'
|
||||
|
||||
// ✅ İYİ: Pahalı hesaplamaları memoize et
|
||||
const sortedMarkets = useMemo(() => {
|
||||
return markets.sort((a, b) => b.volume - a.volume)
|
||||
}, [markets])
|
||||
|
||||
// ✅ İYİ: Callback'leri memoize et
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
setSearchQuery(query)
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
// ✅ İYİ: Ağır bileşenleri lazy yükle
|
||||
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<HeavyChart />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Veritabanı Sorguları
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Sadece gerekli sütunları seç
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('id, name, status')
|
||||
.limit(10)
|
||||
|
||||
// ❌ KÖTÜ: Her şeyi seç
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('*')
|
||||
```
|
||||
|
||||
## Test Standartları
|
||||
|
||||
### Test Yapısı (AAA Kalıbı)
|
||||
|
||||
```typescript
|
||||
test('benzerliği doğru hesaplar', () => {
|
||||
// Arrange (Hazırla)
|
||||
const vector1 = [1, 0, 0]
|
||||
const vector2 = [0, 1, 0]
|
||||
|
||||
// Act (İşle)
|
||||
const similarity = calculateCosineSimilarity(vector1, vector2)
|
||||
|
||||
// Assert (Doğrula)
|
||||
expect(similarity).toBe(0)
|
||||
})
|
||||
```
|
||||
|
||||
### Test İsimlendirme
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Açıklayıcı test isimleri
|
||||
test('sorguya uygun market bulunamadığında boş dizi döndürür', () => { })
|
||||
test('OpenAI API anahtarı eksikse hata fırlatır', () => { })
|
||||
test('Redis kullanılamazsa substring aramaya geri döner', () => { })
|
||||
|
||||
// ❌ KÖTÜ: Belirsiz test isimleri
|
||||
test('çalışır', () => { })
|
||||
test('arama testi', () => { })
|
||||
```
|
||||
|
||||
## Kod Kokusu Tespiti
|
||||
|
||||
Bu anti-kalıplara dikkat edin:
|
||||
|
||||
### 1. Uzun Fonksiyonlar
|
||||
```typescript
|
||||
// ❌ KÖTÜ: 50 satırdan uzun fonksiyon
|
||||
function processMarketData() {
|
||||
// 100 satır kod
|
||||
}
|
||||
|
||||
// ✅ İYİ: Küçük fonksiyonlara böl
|
||||
function processMarketData() {
|
||||
const validated = validateData()
|
||||
const transformed = transformData(validated)
|
||||
return saveData(transformed)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Derin İç İçe Geçme
|
||||
```typescript
|
||||
// ❌ KÖTÜ: 5+ seviye iç içe geçme
|
||||
if (user) {
|
||||
if (user.isAdmin) {
|
||||
if (market) {
|
||||
if (market.isActive) {
|
||||
if (hasPermission) {
|
||||
// Bir şeyler yap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ İYİ: Erken dönüşler
|
||||
if (!user) return
|
||||
if (!user.isAdmin) return
|
||||
if (!market) return
|
||||
if (!market.isActive) return
|
||||
if (!hasPermission) return
|
||||
|
||||
// Bir şeyler yap
|
||||
```
|
||||
|
||||
### 3. Sihirli Sayılar
|
||||
```typescript
|
||||
// ❌ KÖTÜ: Açıklanmamış sayılar
|
||||
if (retryCount > 3) { }
|
||||
setTimeout(callback, 500)
|
||||
|
||||
// ✅ İYİ: İsimlendirilmiş sabitler
|
||||
const MAX_RETRIES = 3
|
||||
const DEBOUNCE_DELAY_MS = 500
|
||||
|
||||
if (retryCount > MAX_RETRIES) { }
|
||||
setTimeout(callback, DEBOUNCE_DELAY_MS)
|
||||
```
|
||||
|
||||
**Unutmayın**: Kod kalitesi pazarlık konusu değildir. Açık, sürdürülebilir kod hızlı geliştirme ve güvenli refactoring sağlar.
|
||||
364
docs/tr/skills/continuous-learning-v2/SKILL.md
Normal file
364
docs/tr/skills/continuous-learning-v2/SKILL.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
name: continuous-learning-v2
|
||||
description: Hook'lar aracılığıyla oturumları gözlemleyen, güven skorlaması ile atomik instinct'ler oluşturan ve bunları skill/command/agent'lara evriltiren instinct tabanlı öğrenme sistemi. v2.1 çapraz proje kontaminasyonunu önlemek için proje kapsamlı instinct'ler ekler.
|
||||
origin: ECC
|
||||
version: 2.1.0
|
||||
---
|
||||
|
||||
# Sürekli Öğrenme v2.1 - Instinct Tabanlı Mimari
|
||||
|
||||
Claude Code oturumlarınızı güven skorlaması ile atomik "instinct'ler" - küçük öğrenilmiş davranışlar - aracılığıyla yeniden kullanılabilir bilgiye dönüştüren gelişmiş bir öğrenme sistemi.
|
||||
|
||||
**v2.1** **proje kapsamlı instinct'ler** ekler — React kalıpları React projenizde kalır, Python kuralları Python projenizde kalır ve evrensel kalıplar (örneğin "her zaman input'u doğrula") global olarak paylaşılır.
|
||||
|
||||
## Ne Zaman Aktifleştirmelisiniz
|
||||
|
||||
- Claude Code oturumlarından otomatik öğrenme ayarlarken
|
||||
- Hook'lar aracılığıyla instinct tabanlı davranış çıkarmayı yapılandırırken
|
||||
- Öğrenilmiş davranışlar için güven eşiklerini ayarlarken
|
||||
- Instinct kütüphanelerini incelerken, dışa veya içe aktarırken
|
||||
- Instinct'leri tam skill'lere, command'lara veya agent'lara evriltirken
|
||||
- Proje kapsamlı vs global instinct'leri yönetirken
|
||||
- Instinct'leri projeden global kapsamına yükseltirken
|
||||
|
||||
## v2.1'deki Yenilikler
|
||||
|
||||
| Özellik | v2.0 | v2.1 |
|
||||
|---------|------|------|
|
||||
| Depolama | Global (~/.claude/homunculus/) | Proje kapsamlı (projects/<hash>/) |
|
||||
| Kapsam | Tüm instinct'ler her yerde geçerli | Proje kapsamlı + global |
|
||||
| Tespit | Yok | git remote URL / repo path |
|
||||
| Yükseltme | Yok | Proje → 2+ projede görülünce global |
|
||||
| Komutlar | 4 (status/evolve/export/import) | 6 (+promote/projects) |
|
||||
| Çapraz proje | Kontaminasyon riski | Varsayılan olarak izole |
|
||||
|
||||
## v2'deki Yenilikler (vs v1)
|
||||
|
||||
| Özellik | v1 | v2 |
|
||||
|---------|----|----|
|
||||
| Gözlem | Stop hook (oturum sonu) | PreToolUse/PostToolUse (%100 güvenilir) |
|
||||
| Analiz | Ana bağlam | Arka plan agent'ı (Haiku) |
|
||||
| Granülerlik | Tam skill'ler | Atomik "instinct'ler" |
|
||||
| Güven | Yok | 0.3-0.9 ağırlıklı |
|
||||
| Evrim | Doğrudan skill'e | Instinct'ler -> kümeleme -> skill/command/agent |
|
||||
| Paylaşım | Yok | Instinct'leri dışa/içe aktar |
|
||||
|
||||
## Instinct Modeli
|
||||
|
||||
Instinct küçük öğrenilmiş bir davranıştır:
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: prefer-functional-style
|
||||
trigger: "yeni fonksiyonlar yazarken"
|
||||
confidence: 0.7
|
||||
domain: "code-style"
|
||||
source: "session-observation"
|
||||
scope: project
|
||||
project_id: "a1b2c3d4e5f6"
|
||||
project_name: "my-react-app"
|
||||
---
|
||||
|
||||
# Fonksiyonel Stili Tercih Et
|
||||
|
||||
## Aksiyon
|
||||
Uygun olduğunda sınıflar yerine fonksiyonel kalıpları kullan.
|
||||
|
||||
## Kanıt
|
||||
- 5 fonksiyonel kalıp tercihinin gözlemlenmesi
|
||||
- Kullanıcı 2025-01-15'te sınıf tabanlı yaklaşımı fonksiyonele düzeltti
|
||||
```
|
||||
|
||||
**Özellikler:**
|
||||
- **Atomik** -- bir tetikleyici, bir aksiyon
|
||||
- **Güven ağırlıklı** -- 0.3 = geçici, 0.9 = neredeyse kesin
|
||||
- **Alan etiketli** -- code-style, testing, git, debugging, workflow, vb.
|
||||
- **Kanıt destekli** -- hangi gözlemlerin oluşturduğunu takip eder
|
||||
- **Kapsam farkında** -- `project` (varsayılan) veya `global`
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
```
|
||||
Oturum Aktivitesi (bir git repo'sunda)
|
||||
|
|
||||
| Hook'lar prompt'ları + tool kullanımını yakalar (%100 güvenilir)
|
||||
| + proje bağlamını tespit eder (git remote / repo path)
|
||||
v
|
||||
+---------------------------------------------+
|
||||
| projects/<project-hash>/observations.jsonl |
|
||||
| (prompt'lar, tool çağrıları, sonuçlar, proje) |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
| Gözlemci agent okur (arka plan, Haiku)
|
||||
v
|
||||
+---------------------------------------------+
|
||||
| KALIP TESPİTİ |
|
||||
| * Kullanıcı düzeltmeleri -> instinct |
|
||||
| * Hata çözümleri -> instinct |
|
||||
| * Tekrarlanan iş akışları -> instinct |
|
||||
| * Kapsam kararı: project mi global mi? |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
| Oluşturur/günceller
|
||||
v
|
||||
+---------------------------------------------+
|
||||
| projects/<project-hash>/instincts/personal/ |
|
||||
| * prefer-functional.yaml (0.7) [project] |
|
||||
| * use-react-hooks.yaml (0.9) [project] |
|
||||
+---------------------------------------------+
|
||||
| instincts/personal/ (GLOBAL) |
|
||||
| * always-validate-input.yaml (0.85) [global]|
|
||||
| * grep-before-edit.yaml (0.6) [global] |
|
||||
+---------------------------------------------+
|
||||
|
|
||||
| /evolve kümeleme + /promote
|
||||
v
|
||||
+---------------------------------------------+
|
||||
| projects/<hash>/evolved/ (proje kapsamlı) |
|
||||
| evolved/ (global) |
|
||||
| * commands/new-feature.md |
|
||||
| * skills/testing-workflow.md |
|
||||
| * agents/refactor-specialist.md |
|
||||
+---------------------------------------------+
|
||||
```
|
||||
|
||||
## Proje Tespiti
|
||||
|
||||
Sistem mevcut projenizi otomatik olarak tespit eder:
|
||||
|
||||
1. **`CLAUDE_PROJECT_DIR` env var** (en yüksek öncelik)
|
||||
2. **`git remote get-url origin`** -- taşınabilir proje ID'si oluşturmak için hash'lenir (farklı makinelerde aynı repo aynı ID'yi alır)
|
||||
3. **`git rev-parse --show-toplevel`** -- repo path kullanan yedek (makineye özgü)
|
||||
4. **Global yedek** -- proje tespit edilemezse, instinct'ler global kapsamına gider
|
||||
|
||||
Her proje 12 karakterlik bir hash ID alır (örn. `a1b2c3d4e5f6`). `~/.claude/homunculus/projects.json` dosyasındaki kayıt dosyası ID'leri insanların okuyabileceği isimlerle eşler.
|
||||
|
||||
## Hızlı Başlangıç
|
||||
|
||||
### 1. Gözlem Hook'larını Aktifleştirin
|
||||
|
||||
`~/.claude/settings.json` dosyanıza ekleyin.
|
||||
|
||||
**Plugin olarak kuruluysa** (önerilen):
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh"
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`~/.claude/skills` dizinine manuel kuruluysa**:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh"
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Dizin Yapısını Başlatın
|
||||
|
||||
Sistem ilk kullanımda dizinleri otomatik oluşturur, ancak manuel olarak da oluşturabilirsiniz:
|
||||
|
||||
```bash
|
||||
# Global dizinler
|
||||
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands},projects}
|
||||
|
||||
# Proje dizinleri hook bir git repo'sunda ilk çalıştığında otomatik oluşturulur
|
||||
```
|
||||
|
||||
### 3. Instinct Komutlarını Kullanın
|
||||
|
||||
```bash
|
||||
/instinct-status # Öğrenilmiş instinct'leri göster (proje + global)
|
||||
/evolve # İlgili instinct'leri skill/command'lara kümele
|
||||
/instinct-export # Instinct'leri dosyaya aktar
|
||||
/instinct-import # Başkalarından instinct'leri içe aktar
|
||||
/promote # Proje instinct'lerini global kapsamına yükselt
|
||||
/projects # Tüm bilinen projeleri ve instinct sayılarını listele
|
||||
```
|
||||
|
||||
## Komutlar
|
||||
|
||||
| Komut | Açıklama |
|
||||
|---------|-------------|
|
||||
| `/instinct-status` | Tüm instinct'leri göster (proje kapsamlı + global) güvenle |
|
||||
| `/evolve` | İlgili instinct'leri skill/command'lara kümele, yükseltme öner |
|
||||
| `/instinct-export` | Instinct'leri dışa aktar (kapsam/alana göre filtrelenebilir) |
|
||||
| `/instinct-import <file>` | Kapsam kontrolü ile instinct'leri içe aktar |
|
||||
| `/promote [id]` | Proje instinct'lerini global kapsamına yükselt |
|
||||
| `/projects` | Tüm bilinen projeleri ve instinct sayılarını listele |
|
||||
|
||||
## Konfigürasyon
|
||||
|
||||
Arka plan gözlemcisini kontrol etmek için `config.json` dosyasını düzenleyin:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.1",
|
||||
"observer": {
|
||||
"enabled": false,
|
||||
"run_interval_minutes": 5,
|
||||
"min_observations_to_analyze": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Anahtar | Varsayılan | Açıklama |
|
||||
|-----|---------|-------------|
|
||||
| `observer.enabled` | `false` | Arka plan gözlemci agent'ını aktifleştir |
|
||||
| `observer.run_interval_minutes` | `5` | Gözlemcinin gözlemleri ne sıklıkla analiz ettiği |
|
||||
| `observer.min_observations_to_analyze` | `20` | Analiz çalışmadan önce minimum gözlem |
|
||||
|
||||
Diğer davranışlar (gözlem yakalama, instinct eşikleri, proje kapsamı, yükseltme kriterleri) `instinct-cli.py` ve `observe.sh` içindeki kod varsayılanları aracılığıyla yapılandırılır.
|
||||
|
||||
## Dosya Yapısı
|
||||
|
||||
```
|
||||
~/.claude/homunculus/
|
||||
+-- identity.json # Profiliniz, teknik seviye
|
||||
+-- projects.json # Kayıt: proje hash -> isim/path/remote
|
||||
+-- observations.jsonl # Global gözlemler (yedek)
|
||||
+-- instincts/
|
||||
| +-- personal/ # Global otomatik öğrenilmiş instinct'ler
|
||||
| +-- inherited/ # Global içe aktarılan instinct'ler
|
||||
+-- evolved/
|
||||
| +-- agents/ # Global oluşturulan agent'lar
|
||||
| +-- skills/ # Global oluşturulan skill'ler
|
||||
| +-- commands/ # Global oluşturulan komutlar
|
||||
+-- projects/
|
||||
+-- a1b2c3d4e5f6/ # Proje hash (git remote URL'den)
|
||||
| +-- project.json # Proje başına metadata yansıması (id/name/root/remote)
|
||||
| +-- observations.jsonl
|
||||
| +-- observations.archive/
|
||||
| +-- instincts/
|
||||
| | +-- personal/ # Projeye özgü otomatik öğrenilmiş
|
||||
| | +-- inherited/ # Projeye özgü içe aktarılan
|
||||
| +-- evolved/
|
||||
| +-- skills/
|
||||
| +-- commands/
|
||||
| +-- agents/
|
||||
+-- f6e5d4c3b2a1/ # Başka bir proje
|
||||
+-- ...
|
||||
```
|
||||
|
||||
## Kapsam Karar Kılavuzu
|
||||
|
||||
| Kalıp Tipi | Kapsam | Örnekler |
|
||||
|-------------|-------|---------|
|
||||
| Dil/framework kuralları | **project** | "React hook'ları kullan", "Django REST kalıplarını takip et" |
|
||||
| Dosya yapısı tercihleri | **project** | "Testler `__tests__`/ içinde", "Bileşenler src/components/ içinde" |
|
||||
| Kod stili | **project** | "Fonksiyonel stil kullan", "Dataclass'ları tercih et" |
|
||||
| Hata işleme stratejileri | **project** | "Hatalar için Result tipi kullan" |
|
||||
| Güvenlik uygulamaları | **global** | "Kullanıcı input'unu doğrula", "SQL'i sanitize et" |
|
||||
| Genel en iyi uygulamalar | **global** | "Önce testleri yaz", "Her zaman hataları işle" |
|
||||
| Tool iş akışı tercihleri | **global** | "Edit'ten önce Grep", "Write'tan önce Read" |
|
||||
| Git uygulamaları | **global** | "Conventional commit'ler", "Küçük odaklı commit'ler" |
|
||||
|
||||
## Instinct Yükseltme (Project -> Global)
|
||||
|
||||
Aynı instinct birden fazla projede yüksek güvenle göründüğünde, global kapsamına yükseltme adayıdır.
|
||||
|
||||
**Otomatik yükseltme kriterleri:**
|
||||
- 2+ projede aynı instinct ID
|
||||
- Ortalama güven >= 0.8
|
||||
|
||||
**Nasıl yükseltilir:**
|
||||
|
||||
```bash
|
||||
# Belirli bir instinct'i yükselt
|
||||
python3 instinct-cli.py promote prefer-explicit-errors
|
||||
|
||||
# Tüm uygun instinct'leri otomatik yükselt
|
||||
python3 instinct-cli.py promote
|
||||
|
||||
# Değişiklik yapmadan önizle
|
||||
python3 instinct-cli.py promote --dry-run
|
||||
```
|
||||
|
||||
`/evolve` komutu ayrıca yükseltme adaylarını önerir.
|
||||
|
||||
## Güven Skorlaması
|
||||
|
||||
Güven zamanla evrimleşir:
|
||||
|
||||
| Skor | Anlamı | Davranış |
|
||||
|-------|---------|----------|
|
||||
| 0.3 | Geçici | Önerilir ama zorunlu değil |
|
||||
| 0.5 | Orta | İlgili olduğunda uygulanır |
|
||||
| 0.7 | Güçlü | Uygulama için otomatik onaylanır |
|
||||
| 0.9 | Neredeyse kesin | Temel davranış |
|
||||
|
||||
**Güven artar** şu durumlarda:
|
||||
- Kalıp tekrar tekrar gözlemlenir
|
||||
- Kullanıcı önerilen davranışı düzeltmez
|
||||
- Diğer kaynaklardan benzer instinct'ler hemfikirdir
|
||||
|
||||
**Güven azalır** şu durumlarda:
|
||||
- Kullanıcı davranışı açıkça düzeltir
|
||||
- Kalıp uzun süre gözlemlenmez
|
||||
- Çelişkili kanıt ortaya çıkar
|
||||
|
||||
## Neden Gözlem için Skill'ler Yerine Hook'lar?
|
||||
|
||||
> "v1 gözlem için skill'lere güveniyordu. Skill'ler olasılıksaldır -- Claude'un yargısına göre zamanın ~%50-80'inde tetiklenirler."
|
||||
|
||||
Hook'lar **%100** deterministik olarak tetiklenir. Bu şu anlama gelir:
|
||||
- Her tool çağrısı gözlemlenir
|
||||
- Hiçbir kalıp kaçırılmaz
|
||||
- Öğrenme kapsamlıdır
|
||||
|
||||
## Geriye Dönük Uyumluluk
|
||||
|
||||
v2.1, v2.0 ve v1 ile tamamen uyumludur:
|
||||
- `~/.claude/homunculus/instincts/` içindeki mevcut global instinct'ler hala global instinct olarak çalışır
|
||||
- v1'den `~/.claude/skills/learned/` skill'leri hala çalışır
|
||||
- Stop hook hala çalışır (ama şimdi v2'ye de beslenir)
|
||||
- Kademeli geçiş: her ikisini de paralel çalıştırın
|
||||
|
||||
## Gizlilik
|
||||
|
||||
- Gözlemler makinenizde **yerel** kalır
|
||||
- Proje kapsamlı instinct'ler proje başına izoledir
|
||||
- Sadece **instinct'ler** (kalıplar) dışa aktarılabilir — ham gözlemler değil
|
||||
- Gerçek kod veya konuşma içeriği paylaşılmaz
|
||||
- Neyin dışa aktarılacağını ve yükseltileceğini siz kontrol edersiniz
|
||||
|
||||
## İlgili
|
||||
|
||||
- [ECC-Tools GitHub App](https://github.com/apps/ecc-tools) - Repo geçmişinden instinct'ler oluştur
|
||||
- Homunculus - v2 instinct tabanlı mimariye ilham veren topluluk projesi (atomik gözlemler, güven skorlaması, instinct evrim hattı)
|
||||
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - Sürekli öğrenme bölümü
|
||||
|
||||
---
|
||||
|
||||
*Instinct tabanlı öğrenme: Claude'a kalıplarınızı öğretmek, her seferinde bir proje.*
|
||||
119
docs/tr/skills/continuous-learning/SKILL.md
Normal file
119
docs/tr/skills/continuous-learning/SKILL.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: continuous-learning
|
||||
description: Claude Code oturumlarından yeniden kullanılabilir kalıpları otomatik olarak çıkarın ve gelecekte kullanmak üzere öğrenilmiş skill'ler olarak kaydedin.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Sürekli Öğrenme Skill'i
|
||||
|
||||
Claude Code oturumlarını sonunda otomatik olarak değerlendirir ve öğrenilmiş skill'ler olarak kaydedilebilecek yeniden kullanılabilir kalıpları çıkarır.
|
||||
|
||||
## Ne Zaman Aktifleştirmelisiniz
|
||||
|
||||
- Claude Code oturumlarından otomatik kalıp çıkarma ayarlarken
|
||||
- Oturum değerlendirmesi için Stop hook'u yapılandırırken
|
||||
- `~/.claude/skills/learned/` içindeki öğrenilmiş skill'leri incelerken veya düzenlerken
|
||||
- Çıkarma eşiklerini veya kalıp kategorilerini ayarlarken
|
||||
- v1 (bu) ile v2 (instinct tabanlı) yaklaşımlarını karşılaştırırken
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
Bu skill her oturumun sonunda **Stop hook** olarak çalışır:
|
||||
|
||||
1. **Oturum Değerlendirmesi**: Oturumun yeterli mesaja sahip olup olmadığını kontrol eder (varsayılan: 10+)
|
||||
2. **Kalıp Tespiti**: Oturumdan çıkarılabilir kalıpları tanımlar
|
||||
3. **Skill Çıkarma**: Yararlı kalıpları `~/.claude/skills/learned/` dizinine kaydeder
|
||||
|
||||
## Konfigürasyon
|
||||
|
||||
Özelleştirmek için `config.json` dosyasını düzenleyin:
|
||||
|
||||
```json
|
||||
{
|
||||
"min_session_length": 10,
|
||||
"extraction_threshold": "medium",
|
||||
"auto_approve": false,
|
||||
"learned_skills_path": "~/.claude/skills/learned/",
|
||||
"patterns_to_detect": [
|
||||
"error_resolution",
|
||||
"user_corrections",
|
||||
"workarounds",
|
||||
"debugging_techniques",
|
||||
"project_specific"
|
||||
],
|
||||
"ignore_patterns": [
|
||||
"simple_typos",
|
||||
"one_time_fixes",
|
||||
"external_api_issues"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Kalıp Tipleri
|
||||
|
||||
| Kalıp | Açıklama |
|
||||
|---------|-------------|
|
||||
| `error_resolution` | Belirli hataların nasıl çözüldüğü |
|
||||
| `user_corrections` | Kullanıcı düzeltmelerinden kalıplar |
|
||||
| `workarounds` | Framework/kütüphane tuhaflıklarına çözümler |
|
||||
| `debugging_techniques` | Etkili hata ayıklama yaklaşımları |
|
||||
| `project_specific` | Projeye özgü kurallar |
|
||||
|
||||
## Hook Kurulumu
|
||||
|
||||
`~/.claude/settings.json` dosyanıza ekleyin:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "~/.claude/skills/continuous-learning/evaluate-session.sh"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Neden Stop Hook?
|
||||
|
||||
- **Hafif**: Oturum sonunda bir kez çalışır
|
||||
- **Bloke Etmeyen**: Her mesaja gecikme eklemez
|
||||
- **Tam Bağlam**: Tam oturum kaydına erişimi vardır
|
||||
|
||||
## İlgili
|
||||
|
||||
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - Sürekli öğrenme bölümü
|
||||
- `/learn` komutu - Oturum ortasında manuel kalıp çıkarma
|
||||
|
||||
---
|
||||
|
||||
## Karşılaştırma Notları (Araştırma: Ocak 2025)
|
||||
|
||||
### vs Homunculus
|
||||
|
||||
Homunculus v2 daha sofistike bir yaklaşım benimsiyor:
|
||||
|
||||
| Özellik | Bizim Yaklaşım | Homunculus v2 |
|
||||
|---------|--------------|---------------|
|
||||
| Gözlem | Stop hook (oturum sonu) | PreToolUse/PostToolUse hooks (%100 güvenilir) |
|
||||
| Analiz | Ana bağlam | Arka plan agent'ı (Haiku) |
|
||||
| Granülerlik | Tam skill'ler | Atomik "instinct'ler" |
|
||||
| Güven | Yok | 0.3-0.9 ağırlıklı |
|
||||
| Evrim | Doğrudan skill'e | Instinct'ler → kümeleme → skill/command/agent |
|
||||
| Paylaşım | Yok | Instinct'leri dışa/içe aktar |
|
||||
|
||||
**Homunculus'tan temel içgörü:**
|
||||
> "v1 gözlem için skill'lere güveniyordu. Skill'ler olasılıksaldır—zamanın ~%50-80'inde tetiklenirler. v2 gözlem için hook'ları kullanır (%100 güvenilir) ve öğrenilmiş davranışın atomik birimi olarak instinct'leri kullanır."
|
||||
|
||||
### Potansiyel v2 İyileştirmeleri
|
||||
|
||||
1. **Instinct tabanlı öğrenme** - Güven skorlaması ile daha küçük, atomik davranışlar
|
||||
2. **Arka plan gözlemcisi** - Paralel analiz yapan Haiku agent'ı
|
||||
3. **Güven azalması** - Çelişkiye uğrarsa instinct'ler güven kaybeder
|
||||
4. **Alan etiketleme** - code-style, testing, git, debugging, vb.
|
||||
5. **Evrim yolu** - İlgili instinct'leri skill/command'lara kümeleme
|
||||
|
||||
Bkz: Tam spec için `docs/continuous-learning-v2-spec.md`.
|
||||
319
docs/tr/skills/database-migrations/SKILL.md
Normal file
319
docs/tr/skills/database-migrations/SKILL.md
Normal file
@@ -0,0 +1,319 @@
|
||||
---
|
||||
name: database-migrations
|
||||
description: Şema değişiklikleri, veri migration'ları, rollback'ler ve PostgreSQL, MySQL ve yaygın ORM'ler (Prisma, Drizzle, Django, TypeORM, golang-migrate) arasında sıfır kesinti deployment'ları için veritabanı migration en iyi uygulamaları.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Veritabanı Migration Kalıpları
|
||||
|
||||
Üretim sistemleri için güvenli, geri alınabilir veritabanı şema değişiklikleri.
|
||||
|
||||
## Ne Zaman Aktifleştirmeli
|
||||
|
||||
- Veritabanı tabloları oluştururken veya değiştirirken
|
||||
- Sütun veya indeks eklerken/kaldırırken
|
||||
- Veri migration'ları çalıştırırken (backfill, dönüştürme)
|
||||
- Sıfır kesinti şema değişiklikleri planlarken
|
||||
- Yeni bir proje için migration araçları kurarken
|
||||
|
||||
## Temel İlkeler
|
||||
|
||||
1. **Her değişiklik bir migration'dır** — üretim veritabanlarını asla manuel olarak değiştirmeyin
|
||||
2. **Migration'lar üretimde sadece ileri** — rollback'ler yeni forward migration'lar kullanır
|
||||
3. **Şema ve veri migration'ları ayrıdır** — tek migration'da DDL ve DML'yi asla karıştırmayın
|
||||
4. **Migration'ları üretim boyutundaki veriye karşı test edin** — 100 satırda çalışan migration 10M'de kilitlenebilir
|
||||
5. **Migration'lar üretimde çalıştıktan sonra değişmezdir** — üretimde çalışan migration'ı asla düzenlemeyin
|
||||
|
||||
## Migration Güvenlik Kontrol Listesi
|
||||
|
||||
Herhangi bir migration uygulamadan önce:
|
||||
|
||||
- [ ] Migration UP ve DOWN'a sahip (veya açıkça geri alınamaz olarak işaretlenmiş)
|
||||
- [ ] Büyük tablolarda tam tablo kilitleri yok (concurrent operasyonlar kullan)
|
||||
- [ ] Yeni sütunlar varsayılanlara sahip veya nullable (varsayılan olmadan NOT NULL asla ekleme)
|
||||
- [ ] İndeksler concurrent oluşturuluyor (mevcut tablolar için CREATE TABLE ile inline değil)
|
||||
- [ ] Veri backfill şema değişikliğinden ayrı bir migration
|
||||
- [ ] Üretim verisinin kopyasına karşı test edilmiş
|
||||
- [ ] Rollback planı dokümante edilmiş
|
||||
|
||||
## PostgreSQL Kalıpları
|
||||
|
||||
### Güvenli Sütun Ekleme
|
||||
|
||||
```sql
|
||||
-- İYİ: Nullable sütun, kilit yok
|
||||
ALTER TABLE users ADD COLUMN avatar_url TEXT;
|
||||
|
||||
-- İYİ: Varsayılanlı sütun (Postgres 11+ anlık, yeniden yazma yok)
|
||||
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- KÖTÜ: Mevcut tabloda varsayılansız NOT NULL (tam yeniden yazma gerektirir)
|
||||
ALTER TABLE users ADD COLUMN role TEXT NOT NULL;
|
||||
-- Bu tabloyu kilitler ve her satırı yeniden yazar
|
||||
```
|
||||
|
||||
### Kesinti Olmadan İndeks Ekleme
|
||||
|
||||
```sql
|
||||
-- KÖTÜ: Büyük tablolarda yazmaları engeller
|
||||
CREATE INDEX idx_users_email ON users (email);
|
||||
|
||||
-- İYİ: Engellemez, concurrent yazmalara izin verir
|
||||
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
|
||||
|
||||
-- Not: CONCURRENTLY transaction bloğu içinde çalıştırılamaz
|
||||
-- Çoğu migration aracı bunun için özel işleme ihtiyaç duyar
|
||||
```
|
||||
|
||||
### Sütun Yeniden Adlandırma (Sıfır Kesinti)
|
||||
|
||||
Üretimde asla doğrudan yeniden adlandırmayın. Expand-contract kalıbını kullanın:
|
||||
|
||||
```sql
|
||||
-- Adım 1: Yeni sütun ekle (migration 001)
|
||||
ALTER TABLE users ADD COLUMN display_name TEXT;
|
||||
|
||||
-- Adım 2: Veriyi backfill et (migration 002, veri migration'ı)
|
||||
UPDATE users SET display_name = username WHERE display_name IS NULL;
|
||||
|
||||
-- Adım 3: Uygulama kodunu her iki sütunu okuma/yazma için güncelle
|
||||
-- Uygulama değişikliklerini deploy et
|
||||
|
||||
-- Adım 4: Eski sütuna yazmayı durdur, kaldır (migration 003)
|
||||
ALTER TABLE users DROP COLUMN username;
|
||||
```
|
||||
|
||||
### Güvenli Sütun Kaldırma
|
||||
|
||||
```sql
|
||||
-- Adım 1: Sütuna tüm uygulama referanslarını kaldır
|
||||
-- Adım 2: Sütun referansı olmadan uygulamayı deploy et
|
||||
-- Adım 3: Sonraki migration'da sütunu kaldır
|
||||
ALTER TABLE orders DROP COLUMN legacy_status;
|
||||
|
||||
-- Django için: SeparateDatabaseAndState kullanarak modelden kaldır
|
||||
-- DROP COLUMN oluşturmadan (sonra sonraki migration'da kaldır)
|
||||
```
|
||||
|
||||
### Büyük Veri Migration'ları
|
||||
|
||||
```sql
|
||||
-- KÖTÜ: Tüm satırları tek transaction'da günceller (tabloyu kilitler)
|
||||
UPDATE users SET normalized_email = LOWER(email);
|
||||
|
||||
-- İYİ: İlerleme ile batch güncelleme
|
||||
DO $$
|
||||
DECLARE
|
||||
batch_size INT := 10000;
|
||||
rows_updated INT;
|
||||
BEGIN
|
||||
LOOP
|
||||
UPDATE users
|
||||
SET normalized_email = LOWER(email)
|
||||
WHERE id IN (
|
||||
SELECT id FROM users
|
||||
WHERE normalized_email IS NULL
|
||||
LIMIT batch_size
|
||||
FOR UPDATE SKIP LOCKED
|
||||
);
|
||||
GET DIAGNOSTICS rows_updated = ROW_COUNT;
|
||||
RAISE NOTICE 'Updated % rows', rows_updated;
|
||||
EXIT WHEN rows_updated = 0;
|
||||
COMMIT;
|
||||
END LOOP;
|
||||
END $$;
|
||||
```
|
||||
|
||||
## Prisma (TypeScript/Node.js)
|
||||
|
||||
### İş Akışı
|
||||
|
||||
```bash
|
||||
# Şema değişikliklerinden migration oluştur
|
||||
npx prisma migrate dev --name add_user_avatar
|
||||
|
||||
# Üretimde bekleyen migration'ları uygula
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Veritabanını sıfırla (sadece dev)
|
||||
npx prisma migrate reset
|
||||
|
||||
# Şema değişikliklerinden sonra client oluştur
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
### Şema Örneği
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
avatarUrl String? @map("avatar_url")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
orders Order[]
|
||||
|
||||
@@map("users")
|
||||
@@index([email])
|
||||
}
|
||||
```
|
||||
|
||||
### Özel SQL Migration
|
||||
|
||||
Prisma'nın ifade edemediği operasyonlar için (concurrent indeksler, veri backfill'leri):
|
||||
|
||||
```bash
|
||||
# Boş migration oluştur, sonra SQL'i manuel düzenle
|
||||
npx prisma migrate dev --create-only --name add_email_index
|
||||
```
|
||||
|
||||
```sql
|
||||
-- migrations/20240115_add_email_index/migration.sql
|
||||
-- Prisma CONCURRENTLY oluşturamaz, bu yüzden manuel yazıyoruz
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users (email);
|
||||
```
|
||||
|
||||
## Drizzle (TypeScript/Node.js)
|
||||
|
||||
### İş Akışı
|
||||
|
||||
```bash
|
||||
# Şema değişikliklerinden migration oluştur
|
||||
npx drizzle-kit generate
|
||||
|
||||
# Migration'ları uygula
|
||||
npx drizzle-kit migrate
|
||||
|
||||
# Şemayı doğrudan push et (sadece dev, migration dosyası yok)
|
||||
npx drizzle-kit push
|
||||
```
|
||||
|
||||
### Şema Örneği
|
||||
|
||||
```typescript
|
||||
import { pgTable, text, timestamp, uuid, boolean } from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
email: text("email").notNull().unique(),
|
||||
name: text("name"),
|
||||
isActive: boolean("is_active").notNull().default(true),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
## Django (Python)
|
||||
|
||||
### İş Akışı
|
||||
|
||||
```bash
|
||||
# Model değişikliklerinden migration oluştur
|
||||
python manage.py makemigrations
|
||||
|
||||
# Migration'ları uygula
|
||||
python manage.py migrate
|
||||
|
||||
# Migration durumunu göster
|
||||
python manage.py showmigrations
|
||||
|
||||
# Özel SQL için boş migration oluştur
|
||||
python manage.py makemigrations --empty app_name -n description
|
||||
```
|
||||
|
||||
### Veri Migration
|
||||
|
||||
```python
|
||||
from django.db import migrations
|
||||
|
||||
def backfill_display_names(apps, schema_editor):
|
||||
User = apps.get_model("accounts", "User")
|
||||
batch_size = 5000
|
||||
users = User.objects.filter(display_name="")
|
||||
while users.exists():
|
||||
batch = list(users[:batch_size])
|
||||
for user in batch:
|
||||
user.display_name = user.username
|
||||
User.objects.bulk_update(batch, ["display_name"], batch_size=batch_size)
|
||||
|
||||
def reverse_backfill(apps, schema_editor):
|
||||
pass # Veri migration'ı, geri alma gerekmez
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("accounts", "0015_add_display_name")]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_display_names, reverse_backfill),
|
||||
]
|
||||
```
|
||||
|
||||
## golang-migrate (Go)
|
||||
|
||||
### İş Akışı
|
||||
|
||||
```bash
|
||||
# Migration çifti oluştur
|
||||
migrate create -ext sql -dir migrations -seq add_user_avatar
|
||||
|
||||
# Tüm bekleyen migration'ları uygula
|
||||
migrate -path migrations -database "$DATABASE_URL" up
|
||||
|
||||
# Son migration'ı rollback et
|
||||
migrate -path migrations -database "$DATABASE_URL" down 1
|
||||
|
||||
# Versiyonu zorla (dirty durumu düzelt)
|
||||
migrate -path migrations -database "$DATABASE_URL" force VERSION
|
||||
```
|
||||
|
||||
### Migration Dosyaları
|
||||
|
||||
```sql
|
||||
-- migrations/000003_add_user_avatar.up.sql
|
||||
ALTER TABLE users ADD COLUMN avatar_url TEXT;
|
||||
CREATE INDEX CONCURRENTLY idx_users_avatar ON users (avatar_url) WHERE avatar_url IS NOT NULL;
|
||||
|
||||
-- migrations/000003_add_user_avatar.down.sql
|
||||
DROP INDEX IF EXISTS idx_users_avatar;
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS avatar_url;
|
||||
```
|
||||
|
||||
## Sıfır Kesinti Migration Stratejisi
|
||||
|
||||
Kritik üretim değişiklikleri için expand-contract kalıbını takip edin:
|
||||
|
||||
```
|
||||
Faz 1: EXPAND
|
||||
- Yeni sütun/tablo ekle (nullable veya varsayılanlı)
|
||||
- Deploy: uygulama hem ESKİ hem YENİ'ye yazar
|
||||
- Mevcut veriyi backfill et
|
||||
|
||||
Faz 2: MIGRATE
|
||||
- Deploy: uygulama YENİ'den okur, her İKİSİNE yazar
|
||||
- Veri tutarlılığını doğrula
|
||||
|
||||
Faz 3: CONTRACT
|
||||
- Deploy: uygulama sadece YENİ'yi kullanır
|
||||
- Eski sütun/tabloyu ayrı migration'da kaldır
|
||||
```
|
||||
|
||||
### Zaman Çizelgesi Örneği
|
||||
|
||||
```
|
||||
Gün 1: Migration new_status sütunu ekler (nullable)
|
||||
Gün 1: App v2 deploy et — hem status hem new_status'a yaz
|
||||
Gün 2: Mevcut satırlar için backfill migration'ı çalıştır
|
||||
Gün 3: App v3 deploy et — sadece new_status'tan okur
|
||||
Gün 7: Migration eski status sütununu kaldırır
|
||||
```
|
||||
|
||||
## Anti-Kalıplar
|
||||
|
||||
| Anti-Kalıp | Neden Başarısız Olur | Daha İyi Yaklaşım |
|
||||
|-------------|-------------|-----------------|
|
||||
| Üretimde manuel SQL | Denetim izi yok, tekrarlanamaz | Her zaman migration dosyaları kullan |
|
||||
| Deploy edilmiş migration'ları düzenleme | Ortamlar arası sapma yaratır | Bunun yerine yeni migration oluştur |
|
||||
| Varsayılansız NOT NULL | Tabloyu kilitler, tüm satırları yeniden yazar | Nullable ekle, backfill et, sonra kısıt ekle |
|
||||
| Büyük tabloda inline indeks | Build sırasında yazmaları engeller | CREATE INDEX CONCURRENTLY |
|
||||
| Tek migration'da şema + veri | Rollback zor, uzun transaction'lar | Ayrı migration'lar |
|
||||
| Kodu kaldırmadan önce sütun kaldırma | Eksik sütunda uygulama hataları | Önce kodu kaldır, sonra sütunu sonraki deploy'da kaldır |
|
||||
427
docs/tr/skills/deployment-patterns/SKILL.md
Normal file
427
docs/tr/skills/deployment-patterns/SKILL.md
Normal file
@@ -0,0 +1,427 @@
|
||||
---
|
||||
name: deployment-patterns
|
||||
description: Deployment iş akışları, CI/CD pipeline kalıpları, Docker konteynerizasyonu, sağlık kontrolleri, rollback stratejileri ve web uygulamaları için üretim hazırlığı kontrol listeleri.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Deployment Kalıpları
|
||||
|
||||
Üretim deployment iş akışları ve CI/CD en iyi uygulamaları.
|
||||
|
||||
## Ne Zaman Aktifleştirmeli
|
||||
|
||||
- CI/CD pipeline'ları kurarken
|
||||
- Bir uygulamayı Docker'ize ederken
|
||||
- Deployment stratejisi planlarken (blue-green, canary, rolling)
|
||||
- Sağlık kontrolleri ve hazırlık probe'ları uygularken
|
||||
- Üretim yayınına hazırlanırken
|
||||
- Ortama özgü ayarları yapılandırırken
|
||||
|
||||
## Deployment Stratejileri
|
||||
|
||||
### Rolling Deployment (Varsayılan)
|
||||
|
||||
Instance'ları kademeli olarak değiştir — rollout sırasında eski ve yeni versiyonlar birlikte çalışır.
|
||||
|
||||
```
|
||||
Instance 1: v1 → v2 (önce güncelle)
|
||||
Instance 2: v1 (hala v1 çalışıyor)
|
||||
Instance 3: v1 (hala v1 çalışıyor)
|
||||
|
||||
Instance 1: v2
|
||||
Instance 2: v1 → v2 (ikinci olarak güncelle)
|
||||
Instance 3: v1
|
||||
|
||||
Instance 1: v2
|
||||
Instance 2: v2
|
||||
Instance 3: v1 → v2 (son olarak güncelle)
|
||||
```
|
||||
|
||||
**Artıları:** Sıfır kesinti, kademeli rollout
|
||||
**Eksileri:** İki versiyon aynı anda çalışır — geriye uyumlu değişiklikler gerektirir
|
||||
**Ne zaman kullanılır:** Standart deployment'lar, geriye uyumlu değişiklikler
|
||||
|
||||
### Blue-Green Deployment
|
||||
|
||||
İki özdeş ortam çalıştır. Trafiği atomik olarak değiştir.
|
||||
|
||||
```
|
||||
Blue (v1) ← trafik
|
||||
Green (v2) boşta, yeni versiyon çalışıyor
|
||||
|
||||
# Doğrulamadan sonra:
|
||||
Blue (v1) boşta (yedek haline gelir)
|
||||
Green (v2) ← trafik
|
||||
```
|
||||
|
||||
**Artıları:** Anında rollback (blue'ya geri dön), temiz geçiş
|
||||
**Eksileri:** Deployment sırasında 2x altyapı gerektirir
|
||||
**Ne zaman kullanılır:** Kritik servisler, sorunlara sıfır tolerans
|
||||
|
||||
### Canary Deployment
|
||||
|
||||
Önce trafiğin küçük bir yüzdesini yeni versiyona yönlendir.
|
||||
|
||||
```
|
||||
v1: %95 trafik
|
||||
v2: %5 trafik (canary)
|
||||
|
||||
# Metrikler iyi görünüyorsa:
|
||||
v1: %50 trafik
|
||||
v2: %50 trafik
|
||||
|
||||
# Final:
|
||||
v2: %100 trafik
|
||||
```
|
||||
|
||||
**Artıları:** Tam rollout'tan önce gerçek trafikle sorunları yakalar
|
||||
**Eksileri:** Trafik bölme altyapısı, izleme gerektirir
|
||||
**Ne zaman kullanılır:** Yüksek trafikli servisler, riskli değişiklikler, feature flag'ler
|
||||
|
||||
## Docker
|
||||
|
||||
### Multi-Stage Dockerfile (Node.js)
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Bağımlılıkları yükle
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --production=false
|
||||
|
||||
# Stage 2: Build
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
RUN npm prune --production
|
||||
|
||||
# Stage 3: Production image
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
|
||||
USER appuser
|
||||
|
||||
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
|
||||
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
||||
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
### Multi-Stage Dockerfile (Go)
|
||||
|
||||
```dockerfile
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
|
||||
|
||||
FROM alpine:3.19 AS runner
|
||||
RUN apk --no-cache add ca-certificates
|
||||
RUN adduser -D -u 1001 appuser
|
||||
USER appuser
|
||||
|
||||
COPY --from=builder /server /server
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:8080/health || exit 1
|
||||
CMD ["/server"]
|
||||
```
|
||||
|
||||
### Multi-Stage Dockerfile (Python/Django)
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim AS builder
|
||||
WORKDIR /app
|
||||
RUN pip install --no-cache-dir uv
|
||||
COPY requirements.txt .
|
||||
RUN uv pip install --system --no-cache -r requirements.txt
|
||||
|
||||
FROM python:3.12-slim AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN useradd -r -u 1001 appuser
|
||||
USER appuser
|
||||
|
||||
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
COPY . .
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')" || exit 1
|
||||
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
|
||||
```
|
||||
|
||||
### Docker En İyi Uygulamaları
|
||||
|
||||
```
|
||||
# İYİ uygulamalar
|
||||
- Belirli versiyon tag'leri kullanın (node:22-alpine, node:latest değil)
|
||||
- Image boyutunu minimize etmek için multi-stage build'ler
|
||||
- Root olmayan kullanıcı olarak çalıştır
|
||||
- Önce bağımlılık dosyalarını kopyalayın (layer caching)
|
||||
- node_modules, .git, test'leri hariç tutmak için .dockerignore kullanın
|
||||
- HEALTHCHECK talimatı ekleyin
|
||||
- docker-compose veya k8s'te kaynak limitleri ayarlayın
|
||||
|
||||
# KÖTÜ uygulamalar
|
||||
- Root olarak çalıştırmak
|
||||
- :latest tag'lerini kullanmak
|
||||
- Tüm repo'yu tek COPY layer'da kopyalamak
|
||||
- Production image'de dev bağımlılıklarını yüklemek
|
||||
- Image'de secret'ları saklamak (env var veya secrets manager kullanın)
|
||||
```
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
### GitHub Actions (Standart Pipeline)
|
||||
|
||||
```yaml
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run typecheck
|
||||
- run: npm test -- --coverage
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage/
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
environment: production
|
||||
steps:
|
||||
- name: Deploy to production
|
||||
run: |
|
||||
# Platforma özgü deployment komutu
|
||||
# Railway: railway up
|
||||
# Vercel: vercel --prod
|
||||
# K8s: kubectl set image deployment/app app=ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
echo "Deploying ${{ github.sha }}"
|
||||
```
|
||||
|
||||
### Pipeline Aşamaları
|
||||
|
||||
```
|
||||
PR açıldığında:
|
||||
lint → typecheck → unit tests → integration tests → preview deploy
|
||||
|
||||
Main'e merge edildiğinde:
|
||||
lint → typecheck → unit tests → integration tests → build image → deploy staging → smoke tests → deploy production
|
||||
```
|
||||
|
||||
## Sağlık Kontrolleri
|
||||
|
||||
### Sağlık Kontrolü Endpoint'i
|
||||
|
||||
```typescript
|
||||
// Basit sağlık kontrolü
|
||||
app.get("/health", (req, res) => {
|
||||
res.status(200).json({ status: "ok" });
|
||||
});
|
||||
|
||||
// Detaylı sağlık kontrolü (dahili izleme için)
|
||||
app.get("/health/detailed", async (req, res) => {
|
||||
const checks = {
|
||||
database: await checkDatabase(),
|
||||
redis: await checkRedis(),
|
||||
externalApi: await checkExternalApi(),
|
||||
};
|
||||
|
||||
const allHealthy = Object.values(checks).every(c => c.status === "ok");
|
||||
|
||||
res.status(allHealthy ? 200 : 503).json({
|
||||
status: allHealthy ? "ok" : "degraded",
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.APP_VERSION || "unknown",
|
||||
uptime: process.uptime(),
|
||||
checks,
|
||||
});
|
||||
});
|
||||
|
||||
async function checkDatabase(): Promise<HealthCheck> {
|
||||
try {
|
||||
await db.query("SELECT 1");
|
||||
return { status: "ok", latency_ms: 2 };
|
||||
} catch (err) {
|
||||
return { status: "error", message: "Database unreachable" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kubernetes Probe'ları
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
failureThreshold: 2
|
||||
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30 # 30 * 5s = 150s max başlatma süresi
|
||||
```
|
||||
|
||||
## Ortam Yapılandırması
|
||||
|
||||
### Twelve-Factor App Kalıbı
|
||||
|
||||
```bash
|
||||
# Tüm yapılandırma ortam değişkenleri ile — asla kodda değil
|
||||
DATABASE_URL=postgres://user:pass@host:5432/db
|
||||
REDIS_URL=redis://host:6379/0
|
||||
API_KEY=${API_KEY} # secrets manager tarafından enjekte edilir
|
||||
LOG_LEVEL=info
|
||||
PORT=3000
|
||||
|
||||
# Ortama özgü davranış
|
||||
NODE_ENV=production # veya staging, development
|
||||
APP_ENV=production # açık uygulama ortamı
|
||||
```
|
||||
|
||||
### Yapılandırma Validasyonu
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
|
||||
const envSchema = z.object({
|
||||
NODE_ENV: z.enum(["development", "staging", "production"]),
|
||||
PORT: z.coerce.number().default(3000),
|
||||
DATABASE_URL: z.string().url(),
|
||||
REDIS_URL: z.string().url(),
|
||||
JWT_SECRET: z.string().min(32),
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
||||
});
|
||||
|
||||
// Başlangıçta validasyon yap — yapılandırma yanlışsa hızlı başarısız ol
|
||||
export const env = envSchema.parse(process.env);
|
||||
```
|
||||
|
||||
## Rollback Stratejisi
|
||||
|
||||
### Anında Rollback
|
||||
|
||||
```bash
|
||||
# Docker/Kubernetes: önceki image'a işaret et
|
||||
kubectl rollout undo deployment/app
|
||||
|
||||
# Vercel: önceki deployment'ı yükselt
|
||||
vercel rollback
|
||||
|
||||
# Railway: önceki commit'i tekrar deploy et
|
||||
railway up --commit <previous-sha>
|
||||
|
||||
# Veritabanı: migration'ı rollback et (geri alınabilirse)
|
||||
npx prisma migrate resolve --rolled-back <migration-name>
|
||||
```
|
||||
|
||||
### Rollback Kontrol Listesi
|
||||
|
||||
- [ ] Önceki image/artifact mevcut ve tag'lenmiş
|
||||
- [ ] Veritabanı migration'ları geriye uyumlu (yıkıcı değişiklik yok)
|
||||
- [ ] Feature flag'ler deploy olmadan yeni özellikleri devre dışı bırakabilir
|
||||
- [ ] Hata oranı artışları için izleme alarmları yapılandırılmış
|
||||
- [ ] Rollback üretim yayınından önce staging'de test edilmiş
|
||||
|
||||
## Üretim Hazırlığı Kontrol Listesi
|
||||
|
||||
Herhangi bir üretim deployment'ından önce:
|
||||
|
||||
### Uygulama
|
||||
- [ ] Tüm testler geçiyor (unit, integration, E2E)
|
||||
- [ ] Kodda veya yapılandırma dosyalarında hardcode edilmiş secret yok
|
||||
- [ ] Hata işleme tüm edge case'leri kapsıyor
|
||||
- [ ] Loglama yapılandırılmış (JSON) ve PII içermiyor
|
||||
- [ ] Sağlık kontrolü endpoint'i anlamlı durum döndürüyor
|
||||
|
||||
### Altyapı
|
||||
- [ ] Docker image yeniden üretilebilir şekilde build oluyor (sabitlenmiş versiyonlar)
|
||||
- [ ] Ortam değişkenleri dokümante edilmiş ve başlangıçta validate ediliyor
|
||||
- [ ] Kaynak limitleri ayarlanmış (CPU, bellek)
|
||||
- [ ] Horizontal scaling yapılandırılmış (min/max instance'lar)
|
||||
- [ ] Tüm endpoint'lerde SSL/TLS etkin
|
||||
|
||||
### İzleme
|
||||
- [ ] Uygulama metrikleri export ediliyor (istek oranı, gecikme, hatalar)
|
||||
- [ ] Hata oranı > eşik için alarmlar yapılandırılmış
|
||||
- [ ] Log toplama kurulmuş (yapılandırılmış loglar, aranabilir)
|
||||
- [ ] Sağlık endpoint'inde uptime izleme
|
||||
|
||||
### Güvenlik
|
||||
- [ ] Bağımlılıklar CVE'ler için taranmış
|
||||
- [ ] CORS sadece izin verilen origin'ler için yapılandırılmış
|
||||
- [ ] Halka açık endpoint'lerde hız sınırlama etkin
|
||||
- [ ] Kimlik doğrulama ve yetkilendirme doğrulanmış
|
||||
- [ ] Güvenlik header'ları ayarlanmış (CSP, HSTS, X-Frame-Options)
|
||||
|
||||
### Operasyonlar
|
||||
- [ ] Rollback planı dokümante edilmiş ve test edilmiş
|
||||
- [ ] Veritabanı migration'ı üretim boyutundaki veriye karşı test edilmiş
|
||||
- [ ] Yaygın hata senaryoları için runbook
|
||||
- [ ] Nöbet rotasyonu ve yükseltme yolu tanımlanmış
|
||||
734
docs/tr/skills/django-patterns/SKILL.md
Normal file
734
docs/tr/skills/django-patterns/SKILL.md
Normal file
@@ -0,0 +1,734 @@
|
||||
---
|
||||
name: django-patterns
|
||||
description: DRF ile Django mimari desenleri, REST API tasarımı, ORM en iyi uygulamaları, caching, signal'ler, middleware ve production-grade Django uygulamaları.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Django Geliştirme Desenleri
|
||||
|
||||
Ölçeklenebilir, bakımı kolay uygulamalar için production-grade Django mimari desenleri.
|
||||
|
||||
## Ne Zaman Etkinleştirmeli
|
||||
|
||||
- Django web uygulamaları oluştururken
|
||||
- Django REST Framework API'leri tasarlarken
|
||||
- Django ORM ve modeller ile çalışırken
|
||||
- Django proje yapısını kurarken
|
||||
- Caching, signal'ler, middleware implement ederken
|
||||
|
||||
## Proje Yapısı
|
||||
|
||||
### Önerilen Düzen
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── config/
|
||||
│ ├── __init__.py
|
||||
│ ├── settings/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py # Base ayarlar
|
||||
│ │ ├── development.py # Dev ayarları
|
||||
│ │ ├── production.py # Production ayarları
|
||||
│ │ └── test.py # Test ayarları
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── manage.py
|
||||
└── apps/
|
||||
├── __init__.py
|
||||
├── users/
|
||||
│ ├── __init__.py
|
||||
│ ├── models.py
|
||||
│ ├── views.py
|
||||
│ ├── serializers.py
|
||||
│ ├── urls.py
|
||||
│ ├── permissions.py
|
||||
│ ├── filters.py
|
||||
│ ├── services.py
|
||||
│ └── tests/
|
||||
└── products/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Split Settings Deseni
|
||||
|
||||
```python
|
||||
# config/settings/base.py
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
SECRET_KEY = env('DJANGO_SECRET_KEY')
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'corsheaders',
|
||||
# Local apps
|
||||
'apps.users',
|
||||
'apps.products',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': env('DB_NAME'),
|
||||
'USER': env('DB_USER'),
|
||||
'PASSWORD': env('DB_PASSWORD'),
|
||||
'HOST': env('DB_HOST'),
|
||||
'PORT': env('DB_PORT', default='5432'),
|
||||
}
|
||||
}
|
||||
|
||||
# config/settings/development.py
|
||||
from .base import *
|
||||
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
|
||||
|
||||
DATABASES['default']['NAME'] = 'myproject_dev'
|
||||
|
||||
INSTALLED_APPS += ['debug_toolbar']
|
||||
|
||||
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# config/settings/production.py
|
||||
from .base import *
|
||||
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
|
||||
# Logging
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'WARNING',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': '/var/log/django/django.log',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'WARNING',
|
||||
'propagate': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Model Tasarım Desenleri
|
||||
|
||||
### Model En İyi Uygulamaları
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
class User(AbstractUser):
|
||||
"""AbstractUser'ı extend eden özel kullanıcı modeli."""
|
||||
email = models.EmailField(unique=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
birth_date = models.DateField(null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'user'
|
||||
verbose_name_plural = 'users'
|
||||
ordering = ['-date_joined']
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
def get_full_name(self):
|
||||
return f"{self.first_name} {self.last_name}".strip()
|
||||
|
||||
class Product(models.Model):
|
||||
"""Uygun alan yapılandırması ile Product modeli."""
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(unique=True, max_length=250)
|
||||
description = models.TextField(blank=True)
|
||||
price = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
validators=[MinValueValidator(0)]
|
||||
)
|
||||
stock = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
category = models.ForeignKey(
|
||||
'Category',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='products'
|
||||
)
|
||||
tags = models.ManyToManyField('Tag', blank=True, related_name='products')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'products'
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['slug']),
|
||||
models.Index(fields=['-created_at']),
|
||||
models.Index(fields=['category', 'is_active']),
|
||||
]
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=models.Q(price__gte=0),
|
||||
name='price_non_negative'
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
```
|
||||
|
||||
### QuerySet En İyi Uygulamaları
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
|
||||
class ProductQuerySet(models.QuerySet):
|
||||
"""Product modeli için özel QuerySet."""
|
||||
|
||||
def active(self):
|
||||
"""Sadece aktif ürünleri döndür."""
|
||||
return self.filter(is_active=True)
|
||||
|
||||
def with_category(self):
|
||||
"""N+1 sorgularını önlemek için ilişkili kategoriyi seç."""
|
||||
return self.select_related('category')
|
||||
|
||||
def with_tags(self):
|
||||
"""Many-to-many ilişkisi için tag'leri prefetch et."""
|
||||
return self.prefetch_related('tags')
|
||||
|
||||
def in_stock(self):
|
||||
"""Stok > 0 olan ürünleri döndür."""
|
||||
return self.filter(stock__gt=0)
|
||||
|
||||
def search(self, query):
|
||||
"""İsim veya açıklamaya göre ürünleri ara."""
|
||||
return self.filter(
|
||||
models.Q(name__icontains=query) |
|
||||
models.Q(description__icontains=query)
|
||||
)
|
||||
|
||||
class Product(models.Model):
|
||||
# ... alanlar ...
|
||||
|
||||
objects = ProductQuerySet.as_manager() # Özel QuerySet kullan
|
||||
|
||||
# Kullanım
|
||||
Product.objects.active().with_category().in_stock()
|
||||
```
|
||||
|
||||
### Manager Metodları
|
||||
|
||||
```python
|
||||
class ProductManager(models.Manager):
|
||||
"""Karmaşık sorgular için özel manager."""
|
||||
|
||||
def get_or_none(self, **kwargs):
|
||||
"""DoesNotExist yerine nesne veya None döndür."""
|
||||
try:
|
||||
return self.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
def create_with_tags(self, name, price, tag_names):
|
||||
"""İlişkili tag'lerle ürün oluştur."""
|
||||
product = self.create(name=name, price=price)
|
||||
tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names]
|
||||
product.tags.set(tags)
|
||||
return product
|
||||
|
||||
def bulk_update_stock(self, product_ids, quantity):
|
||||
"""Birden fazla ürün için toplu stok güncellemesi."""
|
||||
return self.filter(id__in=product_ids).update(stock=quantity)
|
||||
|
||||
# Model'de
|
||||
class Product(models.Model):
|
||||
# ... alanlar ...
|
||||
custom = ProductManager()
|
||||
```
|
||||
|
||||
## Django REST Framework Desenleri
|
||||
|
||||
### Serializer Desenleri
|
||||
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from .models import Product, User
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
"""Product modeli için serializer."""
|
||||
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
average_rating = serializers.FloatField(read_only=True)
|
||||
discount_price = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'description', 'price',
|
||||
'discount_price', 'stock', 'category_name',
|
||||
'average_rating', 'created_at'
|
||||
]
|
||||
read_only_fields = ['id', 'slug', 'created_at']
|
||||
|
||||
def get_discount_price(self, obj):
|
||||
"""Uygulanabilirse indirimli fiyatı hesapla."""
|
||||
if hasattr(obj, 'discount') and obj.discount:
|
||||
return obj.price * (1 - obj.discount.percent / 100)
|
||||
return obj.price
|
||||
|
||||
def validate_price(self, value):
|
||||
"""Fiyatın negatif olmadığından emin ol."""
|
||||
if value < 0:
|
||||
raise serializers.ValidationError("Price cannot be negative.")
|
||||
return value
|
||||
|
||||
class ProductCreateSerializer(serializers.ModelSerializer):
|
||||
"""Ürün oluşturmak için serializer."""
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['name', 'description', 'price', 'stock', 'category']
|
||||
|
||||
def validate(self, data):
|
||||
"""Birden fazla alan için özel validation."""
|
||||
if data['price'] > 10000 and data['stock'] > 100:
|
||||
raise serializers.ValidationError(
|
||||
"Cannot have high-value products with large stock."
|
||||
)
|
||||
return data
|
||||
|
||||
class UserRegistrationSerializer(serializers.ModelSerializer):
|
||||
"""Kullanıcı kaydı için serializer."""
|
||||
|
||||
password = serializers.CharField(
|
||||
write_only=True,
|
||||
required=True,
|
||||
validators=[validate_password],
|
||||
style={'input_type': 'password'}
|
||||
)
|
||||
password_confirm = serializers.CharField(write_only=True, style={'input_type': 'password'})
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['email', 'username', 'password', 'password_confirm']
|
||||
|
||||
def validate(self, data):
|
||||
"""Şifrelerin eşleştiğini doğrula."""
|
||||
if data['password'] != data['password_confirm']:
|
||||
raise serializers.ValidationError({
|
||||
"password_confirm": "Password fields didn't match."
|
||||
})
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Hash'lenmiş şifre ile kullanıcı oluştur."""
|
||||
validated_data.pop('password_confirm')
|
||||
password = validated_data.pop('password')
|
||||
user = User.objects.create(**validated_data)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
```
|
||||
|
||||
### ViewSet Desenleri
|
||||
|
||||
```python
|
||||
from rest_framework import viewsets, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from .models import Product
|
||||
from .serializers import ProductSerializer, ProductCreateSerializer
|
||||
from .permissions import IsOwnerOrReadOnly
|
||||
from .filters import ProductFilter
|
||||
from .services import ProductService
|
||||
|
||||
class ProductViewSet(viewsets.ModelViewSet):
|
||||
"""Product modeli için ViewSet."""
|
||||
|
||||
queryset = Product.objects.select_related('category').prefetch_related('tags')
|
||||
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = ProductFilter
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['price', 'created_at', 'name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Action'a göre uygun serializer döndür."""
|
||||
if self.action == 'create':
|
||||
return ProductCreateSerializer
|
||||
return ProductSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Kullanıcı bağlamı ile kaydet."""
|
||||
serializer.save(created_by=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def featured(self, request):
|
||||
"""Öne çıkan ürünleri döndür."""
|
||||
featured = self.queryset.filter(is_featured=True)[:10]
|
||||
serializer = self.get_serializer(featured, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def purchase(self, request, pk=None):
|
||||
"""Bir ürün satın al."""
|
||||
product = self.get_object()
|
||||
service = ProductService()
|
||||
result = service.purchase(product, request.user)
|
||||
return Response(result, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
|
||||
def my_products(self, request):
|
||||
"""Mevcut kullanıcı tarafından oluşturulan ürünleri döndür."""
|
||||
products = self.queryset.filter(created_by=request.user)
|
||||
page = self.paginate_queryset(products)
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
```
|
||||
|
||||
### Özel Action'lar
|
||||
|
||||
```python
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def add_to_cart(request):
|
||||
"""Kullanıcı sepetine ürün ekle."""
|
||||
product_id = request.data.get('product_id')
|
||||
quantity = request.data.get('quantity', 1)
|
||||
|
||||
try:
|
||||
product = Product.objects.get(id=product_id)
|
||||
except Product.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'Product not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
cart, _ = Cart.objects.get_or_create(user=request.user)
|
||||
CartItem.objects.create(
|
||||
cart=cart,
|
||||
product=product,
|
||||
quantity=quantity
|
||||
)
|
||||
|
||||
return Response({'message': 'Added to cart'}, status=status.HTTP_201_CREATED)
|
||||
```
|
||||
|
||||
## Service Layer Deseni
|
||||
|
||||
```python
|
||||
# apps/orders/services.py
|
||||
from typing import Optional
|
||||
from django.db import transaction
|
||||
from .models import Order, OrderItem
|
||||
|
||||
class OrderService:
|
||||
"""Sipariş ilgili iş mantığı için service layer."""
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create_order(user, cart: Cart) -> Order:
|
||||
"""Sepetten sipariş oluştur."""
|
||||
order = Order.objects.create(
|
||||
user=user,
|
||||
total_price=cart.total_price
|
||||
)
|
||||
|
||||
for item in cart.items.all():
|
||||
OrderItem.objects.create(
|
||||
order=order,
|
||||
product=item.product,
|
||||
quantity=item.quantity,
|
||||
price=item.product.price
|
||||
)
|
||||
|
||||
# Sepeti temizle
|
||||
cart.items.all().delete()
|
||||
|
||||
return order
|
||||
|
||||
@staticmethod
|
||||
def process_payment(order: Order, payment_data: dict) -> bool:
|
||||
"""Sipariş için ödemeyi işle."""
|
||||
# Ödeme gateway entegrasyonu
|
||||
payment = PaymentGateway.charge(
|
||||
amount=order.total_price,
|
||||
token=payment_data['token']
|
||||
)
|
||||
|
||||
if payment.success:
|
||||
order.status = Order.Status.PAID
|
||||
order.save()
|
||||
# Onay email'i gönder
|
||||
OrderService.send_confirmation_email(order)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_confirmation_email(order: Order):
|
||||
"""Sipariş onay email'i gönder."""
|
||||
# Email gönderme mantığı
|
||||
pass
|
||||
```
|
||||
|
||||
## Caching Stratejileri
|
||||
|
||||
### View Seviyesi Caching
|
||||
|
||||
```python
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
@method_decorator(cache_page(60 * 15), name='dispatch') # 15 dakika
|
||||
class ProductListView(generic.ListView):
|
||||
model = Product
|
||||
template_name = 'products/list.html'
|
||||
context_object_name = 'products'
|
||||
```
|
||||
|
||||
### Template Fragment Caching
|
||||
|
||||
```django
|
||||
{% load cache %}
|
||||
{% cache 500 sidebar %}
|
||||
... pahalı sidebar içeriği ...
|
||||
{% endcache %}
|
||||
```
|
||||
|
||||
### Düşük Seviye Caching
|
||||
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
|
||||
def get_featured_products():
|
||||
"""Caching ile öne çıkan ürünleri getir."""
|
||||
cache_key = 'featured_products'
|
||||
products = cache.get(cache_key)
|
||||
|
||||
if products is None:
|
||||
products = list(Product.objects.filter(is_featured=True))
|
||||
cache.set(cache_key, products, timeout=60 * 15) # 15 dakika
|
||||
|
||||
return products
|
||||
```
|
||||
|
||||
### QuerySet Caching
|
||||
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
|
||||
def get_popular_categories():
|
||||
cache_key = 'popular_categories'
|
||||
categories = cache.get(cache_key)
|
||||
|
||||
if categories is None:
|
||||
categories = list(Category.objects.annotate(
|
||||
product_count=Count('products')
|
||||
).filter(product_count__gt=10).order_by('-product_count')[:20])
|
||||
cache.set(cache_key, categories, timeout=60 * 60) # 1 saat
|
||||
|
||||
return categories
|
||||
```
|
||||
|
||||
## Signal'ler
|
||||
|
||||
### Signal Desenleri
|
||||
|
||||
```python
|
||||
# apps/users/signals.py
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Profile
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
"""Kullanıcı oluşturulduğunda profil oluştur."""
|
||||
if created:
|
||||
Profile.objects.create(user=instance)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
"""Kullanıcı kaydedildiğinde profili kaydet."""
|
||||
instance.profile.save()
|
||||
|
||||
# apps/users/apps.py
|
||||
from django.apps import AppConfig
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.users'
|
||||
|
||||
def ready(self):
|
||||
"""Uygulama hazır olduğunda signal'leri import et."""
|
||||
import apps.users.signals
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
### Özel Middleware
|
||||
|
||||
```python
|
||||
# middleware/active_user_middleware.py
|
||||
import time
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
class ActiveUserMiddleware(MiddlewareMixin):
|
||||
"""Aktif kullanıcıları takip etmek için middleware."""
|
||||
|
||||
def process_request(self, request):
|
||||
"""Gelen request'i işle."""
|
||||
if request.user.is_authenticated:
|
||||
# Son aktif zamanı güncelle
|
||||
request.user.last_active = timezone.now()
|
||||
request.user.save(update_fields=['last_active'])
|
||||
|
||||
class RequestLoggingMiddleware(MiddlewareMixin):
|
||||
"""Request'leri loglamak için middleware."""
|
||||
|
||||
def process_request(self, request):
|
||||
"""Request başlangıç zamanını logla."""
|
||||
request.start_time = time.time()
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Request süresini logla."""
|
||||
if hasattr(request, 'start_time'):
|
||||
duration = time.time() - request.start_time
|
||||
logger.info(f'{request.method} {request.path} - {response.status_code} - {duration:.3f}s')
|
||||
return response
|
||||
```
|
||||
|
||||
## Performans Optimizasyonu
|
||||
|
||||
### N+1 Sorgu Önleme
|
||||
|
||||
```python
|
||||
# Kötü - N+1 sorguları
|
||||
products = Product.objects.all()
|
||||
for product in products:
|
||||
print(product.category.name) # Her ürün için ayrı sorgu
|
||||
|
||||
# İyi - select_related ile tek sorgu
|
||||
products = Product.objects.select_related('category').all()
|
||||
for product in products:
|
||||
print(product.category.name)
|
||||
|
||||
# İyi - Many-to-many için prefetch
|
||||
products = Product.objects.prefetch_related('tags').all()
|
||||
for product in products:
|
||||
for tag in product.tags.all():
|
||||
print(tag.name)
|
||||
```
|
||||
|
||||
### Veritabanı İndeksleme
|
||||
|
||||
```python
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=200, db_index=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
category = models.ForeignKey('Category', on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['name']),
|
||||
models.Index(fields=['-created_at']),
|
||||
models.Index(fields=['category', 'created_at']),
|
||||
]
|
||||
```
|
||||
|
||||
### Toplu Operasyonlar
|
||||
|
||||
```python
|
||||
# Toplu oluşturma
|
||||
Product.objects.bulk_create([
|
||||
Product(name=f'Product {i}', price=10.00)
|
||||
for i in range(1000)
|
||||
])
|
||||
|
||||
# Toplu güncelleme
|
||||
products = Product.objects.all()[:100]
|
||||
for product in products:
|
||||
product.is_active = True
|
||||
Product.objects.bulk_update(products, ['is_active'])
|
||||
|
||||
# Toplu silme
|
||||
Product.objects.filter(stock=0).delete()
|
||||
```
|
||||
|
||||
## Hızlı Referans
|
||||
|
||||
| Desen | Açıklama |
|
||||
|-------|----------|
|
||||
| Split settings | Ayrı dev/prod/test ayarları |
|
||||
| Özel QuerySet | Yeniden kullanılabilir sorgu metodları |
|
||||
| Service Layer | İş mantığı ayrımı |
|
||||
| ViewSet | REST API endpoint'leri |
|
||||
| Serializer validation | Request/response dönüşümü |
|
||||
| select_related | Foreign key optimizasyonu |
|
||||
| prefetch_related | Many-to-many optimizasyonu |
|
||||
| Cache first | Pahalı operasyonları cache'le |
|
||||
| Signal'ler | Olay güdümlü aksiyonlar |
|
||||
| Middleware | Request/response işleme |
|
||||
|
||||
Unutmayın: Django birçok kısayol sağlar, ancak production uygulamaları için yapı ve organizasyon kısa koddan daha önemlidir. Bakımı kolay olacak şekilde oluşturun.
|
||||
364
docs/tr/skills/docker-patterns/SKILL.md
Normal file
364
docs/tr/skills/docker-patterns/SKILL.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
name: docker-patterns
|
||||
description: Yerel geliştirme, konteyner güvenliği, ağ, volume stratejileri ve multi-servis orkestrasyon için Docker ve Docker Compose kalıpları.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Docker Kalıpları
|
||||
|
||||
Konteynerize edilmiş geliştirme için Docker ve Docker Compose en iyi uygulamaları.
|
||||
|
||||
## Ne Zaman Aktifleştirmeli
|
||||
|
||||
- Yerel geliştirme için Docker Compose kurarken
|
||||
- Çok konteynerli mimariler tasarlarken
|
||||
- Konteyner ağ veya volume sorunlarını giderirken
|
||||
- Dockerfile'ları güvenlik ve boyut için incelerken
|
||||
- Yerel geliştirmeden konteynerize iş akışına geçerken
|
||||
|
||||
## Yerel Geliştirme için Docker Compose
|
||||
|
||||
### Standart Web Uygulaması Stack'i
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: dev # Multi-stage Dockerfile'ın dev aşamasını kullan
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/app # Hot reload için bind mount
|
||||
- /app/node_modules # Anonim volume -- konteyner bağımlılıklarını korur
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- NODE_ENV=development
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
command: npm run dev
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: app_dev
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
mailpit: # Yerel email testi
|
||||
image: axllent/mailpit
|
||||
ports:
|
||||
- "8025:8025" # Web UI
|
||||
- "1025:1025" # SMTP
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
```
|
||||
|
||||
### Geliştirme vs Üretim Dockerfile
|
||||
|
||||
```dockerfile
|
||||
# Aşama: bağımlılıklar
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Aşama: dev (hot reload, debug araçları)
|
||||
FROM node:22-alpine AS dev
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
# Aşama: build
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build && npm prune --production
|
||||
|
||||
# Aşama: production (minimal image)
|
||||
FROM node:22-alpine AS production
|
||||
WORKDIR /app
|
||||
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
|
||||
USER appuser
|
||||
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
|
||||
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
|
||||
COPY --from=build --chown=appuser:appgroup /app/package.json ./
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
### Override Dosyaları
|
||||
|
||||
```yaml
|
||||
# docker-compose.override.yml (otomatik yüklenir, sadece dev ayarları)
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
- DEBUG=app:*
|
||||
- LOG_LEVEL=debug
|
||||
ports:
|
||||
- "9229:9229" # Node.js debugger
|
||||
|
||||
# docker-compose.prod.yml (üretim için açıkça)
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
target: production
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 512M
|
||||
```
|
||||
|
||||
```bash
|
||||
# Geliştirme (override'ı otomatik yükler)
|
||||
docker compose up
|
||||
|
||||
# Üretim
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## Ağ (Networking)
|
||||
|
||||
### Servis Keşfi
|
||||
|
||||
Aynı Compose ağındaki servisler servis adıyla çözümlenir:
|
||||
```
|
||||
# "app" konteynerinden:
|
||||
postgres://postgres:postgres@db:5432/app_dev # "db" db konteynerine çözümlenir
|
||||
redis://redis:6379/0 # "redis" redis konteynerine çözümlenir
|
||||
```
|
||||
|
||||
### Özel Ağlar
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frontend:
|
||||
networks:
|
||||
- frontend-net
|
||||
|
||||
api:
|
||||
networks:
|
||||
- frontend-net
|
||||
- backend-net
|
||||
|
||||
db:
|
||||
networks:
|
||||
- backend-net # Sadece api'den erişilebilir, frontend'den değil
|
||||
|
||||
networks:
|
||||
frontend-net:
|
||||
backend-net:
|
||||
```
|
||||
|
||||
### Sadece Gereklileri Açığa Çıkarma
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432" # Sadece host'tan erişilebilir, ağdan değil
|
||||
# Üretimde port'ları tamamen çıkar -- sadece Docker ağı içinden erişilebilir
|
||||
```
|
||||
|
||||
## Volume Stratejileri
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
# İsimli volume: konteyner yeniden başlatmalarında kalıcı, Docker tarafından yönetilir
|
||||
pgdata:
|
||||
|
||||
# Bind mount: host dizinini konteynere eşler (geliştirme için)
|
||||
# - ./src:/app/src
|
||||
|
||||
# Anonim volume: bind mount override'ından konteyner tarafından oluşturulan içeriği korur
|
||||
# - /app/node_modules
|
||||
```
|
||||
|
||||
### Yaygın Kalıplar
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
volumes:
|
||||
- .:/app # Kaynak kodu (hot reload için bind mount)
|
||||
- /app/node_modules # Konteyner'ın node_modules'ünü host'tan koru
|
||||
- /app/.next # Build cache'ini koru
|
||||
|
||||
db:
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data # Kalıcı veri
|
||||
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # Init scriptleri
|
||||
```
|
||||
|
||||
## Konteyner Güvenliği
|
||||
|
||||
### Dockerfile Sıkılaştırma
|
||||
|
||||
```dockerfile
|
||||
# 1. Belirli tag'ler kullanın (:latest asla)
|
||||
FROM node:22.12-alpine3.20
|
||||
|
||||
# 2. Root olmayan kullanıcı olarak çalıştır
|
||||
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
|
||||
USER app
|
||||
|
||||
# 3. Capability'leri düşür (compose'da)
|
||||
# 4. Mümkün olduğunda salt okunur kök dosya sistemi
|
||||
# 5. Image layer'larında secret yok
|
||||
```
|
||||
|
||||
### Compose Güvenliği
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /app/.cache
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE # Sadece < 1024 port'lara bind için
|
||||
```
|
||||
|
||||
### Secret Yönetimi
|
||||
|
||||
```yaml
|
||||
# İYİ: Ortam değişkenleri kullanın (runtime'da enjekte edilir)
|
||||
services:
|
||||
app:
|
||||
env_file:
|
||||
- .env # .env'i asla git'e commit etmeyin
|
||||
environment:
|
||||
- API_KEY # Host ortamından miras alır
|
||||
|
||||
# İYİ: Docker secrets (Swarm modu)
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./secrets/db_password.txt
|
||||
|
||||
services:
|
||||
db:
|
||||
secrets:
|
||||
- db_password
|
||||
|
||||
# KÖTÜ: Image'de hardcode
|
||||
# ENV API_KEY=sk-proj-xxxxx # ASLA BUNU YAPMAYIN
|
||||
```
|
||||
|
||||
## .dockerignore
|
||||
|
||||
```
|
||||
node_modules
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
dist
|
||||
coverage
|
||||
*.log
|
||||
.next
|
||||
.cache
|
||||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
README.md
|
||||
tests/
|
||||
```
|
||||
|
||||
## Hata Ayıklama
|
||||
|
||||
### Yaygın Komutlar
|
||||
|
||||
```bash
|
||||
# Logları görüntüle
|
||||
docker compose logs -f app # App loglarını takip et
|
||||
docker compose logs --tail=50 db # db'den son 50 satır
|
||||
|
||||
# Çalışan konteynerde komut çalıştır
|
||||
docker compose exec app sh # app'e shell ile gir
|
||||
docker compose exec db psql -U postgres # postgres'e bağlan
|
||||
|
||||
# İncele
|
||||
docker compose ps # Çalışan servisler
|
||||
docker compose top # Her konteynerdeki işlemler
|
||||
docker stats # Kaynak kullanımı
|
||||
|
||||
# Yeniden build et
|
||||
docker compose up --build # Image'leri yeniden build et
|
||||
docker compose build --no-cache app # Tam rebuild'i zorla
|
||||
|
||||
# Temizle
|
||||
docker compose down # Konteynerleri durdur ve kaldır
|
||||
docker compose down -v # Volume'leri de kaldır (YIKıCı)
|
||||
docker system prune # Kullanılmayan image/konteynerleri kaldır
|
||||
```
|
||||
|
||||
### Ağ Sorunlarını Hata Ayıklama
|
||||
|
||||
```bash
|
||||
# Konteyner içinde DNS çözümlemesini kontrol et
|
||||
docker compose exec app nslookup db
|
||||
|
||||
# Bağlantıyı kontrol et
|
||||
docker compose exec app wget -qO- http://api:3000/health
|
||||
|
||||
# Ağı incele
|
||||
docker network ls
|
||||
docker network inspect <project>_default
|
||||
```
|
||||
|
||||
## Anti-Kalıplar
|
||||
|
||||
```
|
||||
# KÖTÜ: Üretimde orkestrasyon olmadan docker compose kullanma
|
||||
# Üretim çok konteynerli iş yükleri için Kubernetes, ECS veya Docker Swarm kullanın
|
||||
|
||||
# KÖTÜ: Volume olmadan konteynerlerde veri depolama
|
||||
# Konteynerler geçicidir -- volume olmadan yeniden başlatmada tüm veri kaybolur
|
||||
|
||||
# KÖTÜ: Root olarak çalıştırma
|
||||
# Daima root olmayan bir kullanıcı oluşturun ve kullanın
|
||||
|
||||
# KÖTÜ: :latest tag kullanma
|
||||
# Yeniden üretilebilir build'ler için belirli versiyonlara sabitle
|
||||
|
||||
# KÖTÜ: Tüm servisleri içeren tek dev konteyner
|
||||
# Endişeleri ayırın: konteyner başına bir işlem
|
||||
|
||||
# KÖTÜ: Secret'ları docker-compose.yml'e koymak
|
||||
# .env dosyaları (gitignore'lanmış) veya Docker secrets kullanın
|
||||
```
|
||||
326
docs/tr/skills/e2e-testing/SKILL.md
Normal file
326
docs/tr/skills/e2e-testing/SKILL.md
Normal file
@@ -0,0 +1,326 @@
|
||||
---
|
||||
name: e2e-testing
|
||||
description: Playwright E2E test kalıpları, Page Object Model, yapılandırma, CI/CD entegrasyonu, artifact yönetimi ve kararsız test stratejileri.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# E2E Test Kalıpları
|
||||
|
||||
Kararlı, hızlı ve sürdürülebilir E2E test paketleri oluşturmak için kapsamlı Playwright kalıpları.
|
||||
|
||||
## Test Dosyası Organizasyonu
|
||||
|
||||
```
|
||||
tests/
|
||||
├── e2e/
|
||||
│ ├── auth/
|
||||
│ │ ├── login.spec.ts
|
||||
│ │ ├── logout.spec.ts
|
||||
│ │ └── register.spec.ts
|
||||
│ ├── features/
|
||||
│ │ ├── browse.spec.ts
|
||||
│ │ ├── search.spec.ts
|
||||
│ │ └── create.spec.ts
|
||||
│ └── api/
|
||||
│ └── endpoints.spec.ts
|
||||
├── fixtures/
|
||||
│ ├── auth.ts
|
||||
│ └── data.ts
|
||||
└── playwright.config.ts
|
||||
```
|
||||
|
||||
## Page Object Model (POM)
|
||||
|
||||
```typescript
|
||||
import { Page, Locator } from '@playwright/test'
|
||||
|
||||
export class ItemsPage {
|
||||
readonly page: Page
|
||||
readonly searchInput: Locator
|
||||
readonly itemCards: Locator
|
||||
readonly createButton: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
this.searchInput = page.locator('[data-testid="search-input"]')
|
||||
this.itemCards = page.locator('[data-testid="item-card"]')
|
||||
this.createButton = page.locator('[data-testid="create-btn"]')
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/items')
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async search(query: string) {
|
||||
await this.searchInput.fill(query)
|
||||
await this.page.waitForResponse(resp => resp.url().includes('/api/search'))
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async getItemCount() {
|
||||
return await this.itemCards.count()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Yapısı
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { ItemsPage } from '../../pages/ItemsPage'
|
||||
|
||||
test.describe('Item Search', () => {
|
||||
let itemsPage: ItemsPage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
itemsPage = new ItemsPage(page)
|
||||
await itemsPage.goto()
|
||||
})
|
||||
|
||||
test('should search by keyword', async ({ page }) => {
|
||||
await itemsPage.search('test')
|
||||
|
||||
const count = await itemsPage.getItemCount()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
|
||||
await expect(itemsPage.itemCards.first()).toContainText(/test/i)
|
||||
await page.screenshot({ path: 'artifacts/search-results.png' })
|
||||
})
|
||||
|
||||
test('should handle no results', async ({ page }) => {
|
||||
await itemsPage.search('xyznonexistent123')
|
||||
|
||||
await expect(page.locator('[data-testid="no-results"]')).toBeVisible()
|
||||
expect(await itemsPage.getItemCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Playwright Yapılandırması
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
['html', { outputFolder: 'playwright-report' }],
|
||||
['junit', { outputFile: 'playwright-results.xml' }],
|
||||
['json', { outputFile: 'playwright-results.json' }]
|
||||
],
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
actionTimeout: 10000,
|
||||
navigationTimeout: 30000,
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Kararsız Test Kalıpları
|
||||
|
||||
### Karantina
|
||||
|
||||
```typescript
|
||||
test('flaky: complex search', async ({ page }) => {
|
||||
test.fixme(true, 'Flaky - Issue #123')
|
||||
// test kodu...
|
||||
})
|
||||
|
||||
test('conditional skip', async ({ page }) => {
|
||||
test.skip(process.env.CI, 'Flaky in CI - Issue #123')
|
||||
// test kodu...
|
||||
})
|
||||
```
|
||||
|
||||
### Kararsızlığı Belirleme
|
||||
|
||||
```bash
|
||||
npx playwright test tests/search.spec.ts --repeat-each=10
|
||||
npx playwright test tests/search.spec.ts --retries=3
|
||||
```
|
||||
|
||||
### Yaygın Nedenler ve Çözümler
|
||||
|
||||
**Yarış koşulları:**
|
||||
```typescript
|
||||
// Kötü: element'in hazır olduğunu varsayar
|
||||
await page.click('[data-testid="button"]')
|
||||
|
||||
// İyi: otomatik bekleme locator
|
||||
await page.locator('[data-testid="button"]').click()
|
||||
```
|
||||
|
||||
**Ağ zamanlaması:**
|
||||
```typescript
|
||||
// Kötü: keyfi timeout
|
||||
await page.waitForTimeout(5000)
|
||||
|
||||
// İyi: belirli koşulu bekle
|
||||
await page.waitForResponse(resp => resp.url().includes('/api/data'))
|
||||
```
|
||||
|
||||
**Animasyon zamanlaması:**
|
||||
```typescript
|
||||
// Kötü: animasyon sırasında tıkla
|
||||
await page.click('[data-testid="menu-item"]')
|
||||
|
||||
// İyi: kararlılığı bekle
|
||||
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.locator('[data-testid="menu-item"]').click()
|
||||
```
|
||||
|
||||
## Artifact Yönetimi
|
||||
|
||||
### Ekran Görüntüleri
|
||||
|
||||
```typescript
|
||||
await page.screenshot({ path: 'artifacts/after-login.png' })
|
||||
await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true })
|
||||
await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' })
|
||||
```
|
||||
|
||||
### Trace'ler
|
||||
|
||||
```typescript
|
||||
await browser.startTracing(page, {
|
||||
path: 'artifacts/trace.json',
|
||||
screenshots: true,
|
||||
snapshots: true,
|
||||
})
|
||||
// ... test aksiyonları ...
|
||||
await browser.stopTracing()
|
||||
```
|
||||
|
||||
### Video
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts'de
|
||||
use: {
|
||||
video: 'retain-on-failure',
|
||||
videosPath: 'artifacts/videos/'
|
||||
}
|
||||
```
|
||||
|
||||
## CI/CD Entegrasyonu
|
||||
|
||||
```yaml
|
||||
# .github/workflows/e2e.yml
|
||||
name: E2E Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npx playwright test
|
||||
env:
|
||||
BASE_URL: ${{ vars.STAGING_URL }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
```
|
||||
|
||||
## Test Raporu Şablonu
|
||||
|
||||
```markdown
|
||||
# E2E Test Raporu
|
||||
|
||||
**Tarih:** YYYY-MM-DD HH:MM
|
||||
**Süre:** Xd Ys
|
||||
**Durum:** GEÇTİ / BAŞARISIZ
|
||||
|
||||
## Özet
|
||||
- Toplam: X | Geçti: Y (Z%) | Başarısız: A | Kararsız: B | Atlandı: C
|
||||
|
||||
## Başarısız Testler
|
||||
|
||||
### test-adı
|
||||
**Dosya:** `tests/e2e/feature.spec.ts:45`
|
||||
**Hata:** Element'in görünür olması bekleniyordu
|
||||
**Ekran Görüntüsü:** artifacts/failed.png
|
||||
**Önerilen Çözüm:** [açıklama]
|
||||
|
||||
## Artifact'lar
|
||||
- HTML Raporu: playwright-report/index.html
|
||||
- Ekran Görüntüleri: artifacts/*.png
|
||||
- Videolar: artifacts/videos/*.webm
|
||||
- Trace'ler: artifacts/*.zip
|
||||
```
|
||||
|
||||
## Wallet / Web3 Testi
|
||||
|
||||
```typescript
|
||||
test('wallet connection', async ({ page, context }) => {
|
||||
// Wallet provider'ı mock'la
|
||||
await context.addInitScript(() => {
|
||||
window.ethereum = {
|
||||
isMetaMask: true,
|
||||
request: async ({ method }) => {
|
||||
if (method === 'eth_requestAccounts')
|
||||
return ['0x1234567890123456789012345678901234567890']
|
||||
if (method === 'eth_chainId') return '0x1'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
await page.locator('[data-testid="connect-wallet"]').click()
|
||||
await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234')
|
||||
})
|
||||
```
|
||||
|
||||
## Finansal / Kritik Akış Testi
|
||||
|
||||
```typescript
|
||||
test('trade execution', async ({ page }) => {
|
||||
// Üretimde atla — gerçek para
|
||||
test.skip(process.env.NODE_ENV === 'production', 'Skip on production')
|
||||
|
||||
await page.goto('/markets/test-market')
|
||||
await page.locator('[data-testid="position-yes"]').click()
|
||||
await page.locator('[data-testid="trade-amount"]').fill('1.0')
|
||||
|
||||
// Önizlemeyi doğrula
|
||||
const preview = page.locator('[data-testid="trade-preview"]')
|
||||
await expect(preview).toContainText('1.0')
|
||||
|
||||
// Onayla ve blockchain'i bekle
|
||||
await page.locator('[data-testid="confirm-trade"]').click()
|
||||
await page.waitForResponse(
|
||||
resp => resp.url().includes('/api/trade') && resp.status() === 200,
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
|
||||
await expect(page.locator('[data-testid="trade-success"]')).toBeVisible()
|
||||
})
|
||||
```
|
||||
270
docs/tr/skills/eval-harness/SKILL.md
Normal file
270
docs/tr/skills/eval-harness/SKILL.md
Normal file
@@ -0,0 +1,270 @@
|
||||
---
|
||||
name: eval-harness
|
||||
description: Eval-driven development (EDD) ilkelerini uygulayan Claude Code oturumları için formal değerlendirme çerçevesi
|
||||
origin: ECC
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
# Eval Harness Skill
|
||||
|
||||
Claude Code oturumları için eval-driven development (EDD) ilkelerini uygulayan formal değerlendirme çerçevesi.
|
||||
|
||||
## Ne Zaman Aktifleştirmeli
|
||||
|
||||
- AI destekli iş akışları için eval-driven development (EDD) kurarken
|
||||
- Claude Code görev tamamlama için geçti/kaldı kriterleri tanımlarken
|
||||
- pass@k metrikleriyle agent güvenilirliğini ölçerken
|
||||
- Prompt veya agent değişiklikleri için regresyon test paketleri oluştururken
|
||||
- Model versiyonları arasında agent performansını benchmark ederken
|
||||
|
||||
## Felsefe
|
||||
|
||||
Eval-Driven Development, eval'ları "AI geliştirmenin birim testleri" olarak ele alır:
|
||||
- İmplementasyondan ÖNCE beklenen davranışı tanımla
|
||||
- Geliştirme sırasında eval'ları sürekli çalıştır
|
||||
- Her değişiklikle regresyonları izle
|
||||
- Güvenilirlik ölçümü için pass@k metriklerini kullan
|
||||
|
||||
## Eval Tipleri
|
||||
|
||||
### Capability Eval'ları
|
||||
Claude'un daha önce yapamadığı bir şeyi yapıp yapamadığını test et:
|
||||
```markdown
|
||||
[CAPABILITY EVAL: feature-name]
|
||||
Görev: Claude'un başarması gereken şeyin açıklaması
|
||||
Başarı Kriterleri:
|
||||
- [ ] Kriter 1
|
||||
- [ ] Kriter 2
|
||||
- [ ] Kriter 3
|
||||
Beklenen Çıktı: Beklenen sonucun açıklaması
|
||||
```
|
||||
|
||||
### Regression Eval'ları
|
||||
Değişikliklerin mevcut fonksiyonaliteyi bozmadığından emin ol:
|
||||
```markdown
|
||||
[REGRESSION EVAL: feature-name]
|
||||
Baseline: SHA veya checkpoint adı
|
||||
Testler:
|
||||
- existing-test-1: PASS/FAIL
|
||||
- existing-test-2: PASS/FAIL
|
||||
- existing-test-3: PASS/FAIL
|
||||
Sonuç: X/Y geçti (önceden Y/Y)
|
||||
```
|
||||
|
||||
## Grader Tipleri
|
||||
|
||||
### 1. Code-Based Grader
|
||||
Kod kullanarak deterministik kontroller:
|
||||
```bash
|
||||
# Dosyanın beklenen pattern içerip içermediğini kontrol et
|
||||
grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL"
|
||||
|
||||
# Testlerin geçip geçmediğini kontrol et
|
||||
npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL"
|
||||
|
||||
# Build'in başarılı olup olmadığını kontrol et
|
||||
npm run build && echo "PASS" || echo "FAIL"
|
||||
```
|
||||
|
||||
### 2. Model-Based Grader
|
||||
Açık uçlu çıktıları değerlendirmek için Claude kullan:
|
||||
```markdown
|
||||
[MODEL GRADER PROMPT]
|
||||
Aşağıdaki kod değişikliğini değerlendir:
|
||||
1. Belirtilen sorunu çözüyor mu?
|
||||
2. İyi yapılandırılmış mı?
|
||||
3. Edge case'ler işleniyor mu?
|
||||
4. Hata işleme uygun mu?
|
||||
|
||||
Puan: 1-5 (1=kötü, 5=mükemmel)
|
||||
Gerekçe: [açıklama]
|
||||
```
|
||||
|
||||
### 3. Human Grader
|
||||
Manuel inceleme için işaretle:
|
||||
```markdown
|
||||
[HUMAN REVIEW REQUIRED]
|
||||
Değişiklik: Neyin değiştiğinin açıklaması
|
||||
Sebep: Neden insan incelemesi gerekli
|
||||
Risk Seviyesi: DÜŞÜK/ORTA/YÜKSEK
|
||||
```
|
||||
|
||||
## Metrikler
|
||||
|
||||
### pass@k
|
||||
"k denemede en az bir başarı"
|
||||
- pass@1: İlk deneme başarı oranı
|
||||
- pass@3: 3 denemede başarı
|
||||
- Tipik hedef: pass@3 > %90
|
||||
|
||||
### pass^k
|
||||
"Tüm k denemeler başarılı"
|
||||
- Güvenilirlik için daha yüksek çıta
|
||||
- pass^3: Ardışık 3 başarı
|
||||
- Kritik yollar için kullan
|
||||
|
||||
## Eval İş Akışı
|
||||
|
||||
### 1. Tanımla (Kodlamadan Önce)
|
||||
```markdown
|
||||
## EVAL DEFINITION: feature-xyz
|
||||
|
||||
### Capability Eval'ları
|
||||
1. Yeni kullanıcı hesabı oluşturabilir
|
||||
2. Email formatını doğrulayabilir
|
||||
3. Şifreyi güvenli şekilde hash'leyebilir
|
||||
|
||||
### Regression Eval'ları
|
||||
1. Mevcut login hala çalışıyor
|
||||
2. Oturum yönetimi değişmedi
|
||||
3. Logout akışı sağlam
|
||||
|
||||
### Başarı Metrikleri
|
||||
- capability eval'lar için pass@3 > %90
|
||||
- regression eval'lar için pass^3 = %100
|
||||
```
|
||||
|
||||
### 2. Uygula
|
||||
Tanımlanan eval'ları geçmek için kod yaz.
|
||||
|
||||
### 3. Değerlendir
|
||||
```bash
|
||||
# Capability eval'ları çalıştır
|
||||
[Her capability eval'ı çalıştır, PASS/FAIL kaydet]
|
||||
|
||||
# Regression eval'ları çalıştır
|
||||
npm test -- --testPathPattern="existing"
|
||||
|
||||
# Rapor oluştur
|
||||
```
|
||||
|
||||
### 4. Rapor
|
||||
```markdown
|
||||
EVAL REPORT: feature-xyz
|
||||
========================
|
||||
|
||||
Capability Eval'ları:
|
||||
create-user: PASS (pass@1)
|
||||
validate-email: PASS (pass@2)
|
||||
hash-password: PASS (pass@1)
|
||||
Genel: 3/3 geçti
|
||||
|
||||
Regression Eval'ları:
|
||||
login-flow: PASS
|
||||
session-mgmt: PASS
|
||||
logout-flow: PASS
|
||||
Genel: 3/3 geçti
|
||||
|
||||
Metrikler:
|
||||
pass@1: %67 (2/3)
|
||||
pass@3: %100 (3/3)
|
||||
|
||||
Durum: İNCELEMEYE HAZIR
|
||||
```
|
||||
|
||||
## Entegrasyon Kalıpları
|
||||
|
||||
### İmplementasyondan Önce
|
||||
```
|
||||
/eval define feature-name
|
||||
```
|
||||
`.claude/evals/feature-name.md` konumunda eval tanım dosyası oluşturur
|
||||
|
||||
### İmplementasyon Sırasında
|
||||
```
|
||||
/eval check feature-name
|
||||
```
|
||||
Mevcut eval'ları çalıştırır ve durumu raporlar
|
||||
|
||||
### İmplementasyondan Sonra
|
||||
```
|
||||
/eval report feature-name
|
||||
```
|
||||
Tam eval raporu oluşturur
|
||||
|
||||
## Eval Depolama
|
||||
|
||||
Eval'ları projede sakla:
|
||||
```
|
||||
.claude/
|
||||
evals/
|
||||
feature-xyz.md # Eval tanımı
|
||||
feature-xyz.log # Eval çalıştırma geçmişi
|
||||
baseline.json # Regression baseline'ları
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
1. **Kodlamadan ÖNCE eval'ları tanımla** - Başarı kriterleri hakkında net düşünmeyi zorlar
|
||||
2. **Eval'ları sık çalıştır** - Regresyonları erken yakala
|
||||
3. **pass@k'yı zaman içinde izle** - Güvenilirlik trendlerini gözle
|
||||
4. **Mümkün olduğunda code grader kullan** - Deterministik > olasılıksal
|
||||
5. **Güvenlik için insan incelemesi** - Güvenlik kontrollerini asla tam otomatikleştirme
|
||||
6. **Eval'ları hızlı tut** - Yavaş eval'lar çalıştırılmaz
|
||||
7. **Eval'ları kodla versiyonla** - Eval'lar birinci sınıf artifact'lardır
|
||||
|
||||
## Örnek: Kimlik Doğrulama Ekleme
|
||||
|
||||
```markdown
|
||||
## EVAL: add-authentication
|
||||
|
||||
### Faz 1: Tanımla (10 dk)
|
||||
Capability Eval'ları:
|
||||
- [ ] Kullanıcı email/şifre ile kayıt olabilir
|
||||
- [ ] Kullanıcı geçerli kimlik bilgileriyle giriş yapabilir
|
||||
- [ ] Geçersiz kimlik bilgileri uygun hatayla reddedilir
|
||||
- [ ] Oturumlar sayfa yeniden yüklemelerinde kalıcıdır
|
||||
- [ ] Logout oturumu temizler
|
||||
|
||||
Regression Eval'ları:
|
||||
- [ ] Halka açık rotalar hala erişilebilir
|
||||
- [ ] API yanıtları değişmedi
|
||||
- [ ] Veritabanı şeması uyumlu
|
||||
|
||||
### Faz 2: Uygula (değişir)
|
||||
[Kod yaz]
|
||||
|
||||
### Faz 3: Değerlendir
|
||||
Çalıştır: /eval check add-authentication
|
||||
|
||||
### Faz 4: Raporla
|
||||
EVAL REPORT: add-authentication
|
||||
==============================
|
||||
Capability: 5/5 geçti (pass@3: %100)
|
||||
Regression: 3/3 geçti (pass^3: %100)
|
||||
Durum: YAYINLA
|
||||
```
|
||||
|
||||
## Product Eval'ları (v1.8)
|
||||
|
||||
Davranış kalitesi sadece birim testlerle yakalanamadığında product eval'ları kullan.
|
||||
|
||||
### Grader Tipleri
|
||||
|
||||
1. Code grader (deterministik assertion'lar)
|
||||
2. Rule grader (regex/şema kısıtlamaları)
|
||||
3. Model grader (LLM-as-judge rubric)
|
||||
4. Human grader (belirsiz çıktılar için manuel karar)
|
||||
|
||||
### pass@k Kılavuzu
|
||||
|
||||
- `pass@1`: doğrudan güvenilirlik
|
||||
- `pass@3`: kontrollü yeniden denemeler altında pratik güvenilirlik
|
||||
- `pass^3`: kararlılık testi (3 çalıştırmanın tümü geçmeli)
|
||||
|
||||
Önerilen eşikler:
|
||||
- Capability eval'ları: pass@3 >= 0.90
|
||||
- Regression eval'ları: yayın-kritik yollar için pass^3 = 1.00
|
||||
|
||||
### Eval Anti-Kalıpları
|
||||
|
||||
- Prompt'ları bilinen eval örneklerine overfitting yapmak
|
||||
- Sadece mutlu-yol çıktılarını ölçmek
|
||||
- Geçme oranlarını kovalamken maliyet ve gecikme kaymasını görmezden gelmek
|
||||
- Yayın kapılarında kararsız grader'lara izin vermek
|
||||
|
||||
### Minimal Eval Artifact Düzeni
|
||||
|
||||
- `.claude/evals/<feature>.md` tanımı
|
||||
- `.claude/evals/<feature>.log` çalıştırma geçmişi
|
||||
- `docs/releases/<version>/eval-summary.md` yayın snapshot'ı
|
||||
642
docs/tr/skills/frontend-patterns/SKILL.md
Normal file
642
docs/tr/skills/frontend-patterns/SKILL.md
Normal file
@@ -0,0 +1,642 @@
|
||||
---
|
||||
name: frontend-patterns
|
||||
description: React, Next.js, state yönetimi, performans optimizasyonu ve UI en iyi uygulamaları için frontend geliştirme kalıpları.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Frontend Geliştirme Kalıpları
|
||||
|
||||
React, Next.js ve performanslı kullanıcı arayüzleri için modern frontend kalıpları.
|
||||
|
||||
## Ne Zaman Aktifleştirmelisiniz
|
||||
|
||||
- React bileşenleri oluştururken (composition, props, rendering)
|
||||
- State yönetirken (useState, useReducer, Zustand, Context)
|
||||
- Veri çekme implementasyonu (SWR, React Query, server components)
|
||||
- Performans optimize ederken (memoization, virtualization, code splitting)
|
||||
- Formlarla çalışırken (validation, controlled inputs, Zod schemas)
|
||||
- Client-side routing ve navigasyon işlerken
|
||||
- Erişilebilir, responsive UI kalıpları oluştururken
|
||||
|
||||
## Bileşen Kalıpları
|
||||
|
||||
### Kalıtım Yerine Composition
|
||||
|
||||
```typescript
|
||||
// ✅ İYİ: Bileşen composition
|
||||
interface CardProps {
|
||||
children: React.ReactNode
|
||||
variant?: 'default' | 'outlined'
|
||||
}
|
||||
|
||||
export function Card({ children, variant = 'default' }: CardProps) {
|
||||
return <div className={`card card-${variant}`}>{children}</div>
|
||||
}
|
||||
|
||||
export function CardHeader({ children }: { children: React.ReactNode }) {
|
||||
return <div className="card-header">{children}</div>
|
||||
}
|
||||
|
||||
export function CardBody({ children }: { children: React.ReactNode }) {
|
||||
return <div className="card-body">{children}</div>
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
<Card>
|
||||
<CardHeader>Başlık</CardHeader>
|
||||
<CardBody>İçerik</CardBody>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Compound Components
|
||||
|
||||
```typescript
|
||||
interface TabsContextValue {
|
||||
activeTab: string
|
||||
setActiveTab: (tab: string) => void
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
|
||||
|
||||
export function Tabs({ children, defaultTab }: {
|
||||
children: React.ReactNode
|
||||
defaultTab: string
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState(defaultTab)
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function TabList({ children }: { children: React.ReactNode }) {
|
||||
return <div className="tab-list">{children}</div>
|
||||
}
|
||||
|
||||
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
|
||||
const context = useContext(TabsContext)
|
||||
if (!context) throw new Error('Tab must be used within Tabs')
|
||||
|
||||
return (
|
||||
<button
|
||||
className={context.activeTab === id ? 'active' : ''}
|
||||
onClick={() => context.setActiveTab(id)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
<Tabs defaultTab="overview">
|
||||
<TabList>
|
||||
<Tab id="overview">Genel Bakış</Tab>
|
||||
<Tab id="details">Detaylar</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
### Render Props Kalıbı
|
||||
|
||||
```typescript
|
||||
interface DataLoaderProps<T> {
|
||||
url: string
|
||||
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
|
||||
}
|
||||
|
||||
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setLoading(false))
|
||||
}, [url])
|
||||
|
||||
return <>{children(data, loading, error)}</>
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
<DataLoader<Market[]> url="/api/markets">
|
||||
{(markets, loading, error) => {
|
||||
if (loading) return <Spinner />
|
||||
if (error) return <Error error={error} />
|
||||
return <MarketList markets={markets!} />
|
||||
}}
|
||||
</DataLoader>
|
||||
```
|
||||
|
||||
## Özel Hook Kalıpları
|
||||
|
||||
### State Yönetimi Hook'u
|
||||
|
||||
```typescript
|
||||
export function useToggle(initialValue = false): [boolean, () => void] {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setValue(v => !v)
|
||||
}, [])
|
||||
|
||||
return [value, toggle]
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
const [isOpen, toggleOpen] = useToggle()
|
||||
```
|
||||
|
||||
### Async Veri Çekme Hook'u
|
||||
|
||||
```typescript
|
||||
interface UseQueryOptions<T> {
|
||||
onSuccess?: (data: T) => void
|
||||
onError?: (error: Error) => void
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useQuery<T>(
|
||||
key: string,
|
||||
fetcher: () => Promise<T>,
|
||||
options?: UseQueryOptions<T>
|
||||
) {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await fetcher()
|
||||
setData(result)
|
||||
options?.onSuccess?.(result)
|
||||
} catch (err) {
|
||||
const error = err as Error
|
||||
setError(error)
|
||||
options?.onError?.(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [fetcher, options])
|
||||
|
||||
useEffect(() => {
|
||||
if (options?.enabled !== false) {
|
||||
refetch()
|
||||
}
|
||||
}, [key, refetch, options?.enabled])
|
||||
|
||||
return { data, error, loading, refetch }
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
const { data: markets, loading, error, refetch } = useQuery(
|
||||
'markets',
|
||||
() => fetch('/api/markets').then(r => r.json()),
|
||||
{
|
||||
onSuccess: data => console.log('Getirilen', data.length, 'market'),
|
||||
onError: err => console.error('Başarısız:', err)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Debounce Hook'u
|
||||
|
||||
```typescript
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => clearTimeout(handler)
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debouncedQuery = useDebounce(searchQuery, 500)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedQuery) {
|
||||
performSearch(debouncedQuery)
|
||||
}
|
||||
}, [debouncedQuery])
|
||||
```
|
||||
|
||||
## State Yönetimi Kalıpları
|
||||
|
||||
### Context + Reducer Kalıbı
|
||||
|
||||
```typescript
|
||||
interface State {
|
||||
markets: Market[]
|
||||
selectedMarket: Market | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_MARKETS'; payload: Market[] }
|
||||
| { type: 'SELECT_MARKET'; payload: Market }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'SET_MARKETS':
|
||||
return { ...state, markets: action.payload }
|
||||
case 'SELECT_MARKET':
|
||||
return { ...state, selectedMarket: action.payload }
|
||||
case 'SET_LOADING':
|
||||
return { ...state, loading: action.payload }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const MarketContext = createContext<{
|
||||
state: State
|
||||
dispatch: Dispatch<Action>
|
||||
} | undefined>(undefined)
|
||||
|
||||
export function MarketProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
markets: [],
|
||||
selectedMarket: null,
|
||||
loading: false
|
||||
})
|
||||
|
||||
return (
|
||||
<MarketContext.Provider value={{ state, dispatch }}>
|
||||
{children}
|
||||
</MarketContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useMarkets() {
|
||||
const context = useContext(MarketContext)
|
||||
if (!context) throw new Error('useMarkets must be used within MarketProvider')
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
## Performans Optimizasyonu
|
||||
|
||||
### Memoization
|
||||
|
||||
```typescript
|
||||
// ✅ Pahalı hesaplamalar için useMemo
|
||||
const sortedMarkets = useMemo(() => {
|
||||
return markets.sort((a, b) => b.volume - a.volume)
|
||||
}, [markets])
|
||||
|
||||
// ✅ Alt bileşenlere geçirilen fonksiyonlar için useCallback
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
setSearchQuery(query)
|
||||
}, [])
|
||||
|
||||
// ✅ Pure bileşenler için React.memo
|
||||
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
|
||||
return (
|
||||
<div className="market-card">
|
||||
<h3>{market.name}</h3>
|
||||
<p>{market.description}</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
### Code Splitting ve Lazy Loading
|
||||
|
||||
```typescript
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
// ✅ Ağır bileşenleri lazy yükle
|
||||
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback={<ChartSkeleton />}>
|
||||
<HeavyChart data={data} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<ThreeJsBackground />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Uzun Listeler için Virtualization
|
||||
|
||||
```typescript
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
|
||||
export function VirtualMarketList({ markets }: { markets: Market[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: markets.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 100, // Tahmini satır yüksekliği
|
||||
overscan: 5 // Ekstra render edilecek öğeler
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map(virtualRow => (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`
|
||||
}}
|
||||
>
|
||||
<MarketCard market={markets[virtualRow.index]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Form İşleme Kalıpları
|
||||
|
||||
### Doğrulamalı Controlled Form
|
||||
|
||||
```typescript
|
||||
interface FormData {
|
||||
name: string
|
||||
description: string
|
||||
endDate: string
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
name?: string
|
||||
description?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export function CreateMarketForm() {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
endDate: ''
|
||||
})
|
||||
|
||||
const [errors, setErrors] = useState<FormErrors>({})
|
||||
|
||||
const validate = (): boolean => {
|
||||
const newErrors: FormErrors = {}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'İsim gereklidir'
|
||||
} else if (formData.name.length > 200) {
|
||||
newErrors.name = 'İsim 200 karakterden az olmalıdır'
|
||||
}
|
||||
|
||||
if (!formData.description.trim()) {
|
||||
newErrors.description = 'Açıklama gereklidir'
|
||||
}
|
||||
|
||||
if (!formData.endDate) {
|
||||
newErrors.endDate = 'Bitiş tarihi gereklidir'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!validate()) return
|
||||
|
||||
try {
|
||||
await createMarket(formData)
|
||||
// Başarı işleme
|
||||
} catch (error) {
|
||||
// Hata işleme
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
value={formData.name}
|
||||
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Market ismi"
|
||||
/>
|
||||
{errors.name && <span className="error">{errors.name}</span>}
|
||||
|
||||
{/* Diğer alanlar */}
|
||||
|
||||
<button type="submit">Market Oluştur</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Boundary Kalıbı
|
||||
|
||||
```typescript
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode },
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
state: ErrorBoundaryState = {
|
||||
hasError: false,
|
||||
error: null
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Error boundary caught:', error, errorInfo)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="error-fallback">
|
||||
<h2>Bir şeyler yanlış gitti</h2>
|
||||
<p>{this.state.error?.message}</p>
|
||||
<button onClick={() => this.setState({ hasError: false })}>
|
||||
Tekrar dene
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
## Animasyon Kalıpları
|
||||
|
||||
### Framer Motion Animasyonları
|
||||
|
||||
```typescript
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
// ✅ Liste animasyonları
|
||||
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{markets.map(market => (
|
||||
<motion.div
|
||||
key={market.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<MarketCard market={market} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ Modal animasyonları
|
||||
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
className="modal-overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<motion.div
|
||||
className="modal-content"
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Erişilebilirlik Kalıpları
|
||||
|
||||
### Klavye Navigasyonu
|
||||
|
||||
```typescript
|
||||
export function Dropdown({ options, onSelect }: DropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setActiveIndex(i => Math.min(i + 1, options.length - 1))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setActiveIndex(i => Math.max(i - 1, 0))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
onSelect(options[activeIndex])
|
||||
setIsOpen(false)
|
||||
break
|
||||
case 'Escape':
|
||||
setIsOpen(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="combobox"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Dropdown implementasyonu */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Focus Yönetimi
|
||||
|
||||
```typescript
|
||||
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Şu anki focus'lanmış elementi kaydet
|
||||
previousFocusRef.current = document.activeElement as HTMLElement
|
||||
|
||||
// Modal'a focus yap
|
||||
modalRef.current?.focus()
|
||||
} else {
|
||||
// Kapatırken focus'u geri yükle
|
||||
previousFocusRef.current?.focus()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return isOpen ? (
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabIndex={-1}
|
||||
onKeyDown={e => e.key === 'Escape' && onClose()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
```
|
||||
|
||||
**Unutmayın**: Modern frontend kalıpları sürdürülebilir, performanslı kullanıcı arayüzleri sağlar. Proje karmaşıklığınıza uyan kalıpları seçin.
|
||||
674
docs/tr/skills/golang-patterns/SKILL.md
Normal file
674
docs/tr/skills/golang-patterns/SKILL.md
Normal file
@@ -0,0 +1,674 @@
|
||||
---
|
||||
name: golang-patterns
|
||||
description: İdiomatic Go desenler, en iyi uygulamalar ve sağlam, verimli ve bakımı kolay Go uygulamaları oluşturmak için konvansiyonlar.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Go Geliştirme Desenleri
|
||||
|
||||
Sağlam, verimli ve bakımı kolay uygulamalar oluşturmak için idiomatic Go desenleri ve en iyi uygulamalar.
|
||||
|
||||
## Ne Zaman Etkinleştirmeli
|
||||
|
||||
- Yeni Go kodu yazarken
|
||||
- Go kodunu gözden geçirirken
|
||||
- Mevcut Go kodunu refactor ederken
|
||||
- Go paketleri/modülleri tasarlarken
|
||||
|
||||
## Temel Prensipler
|
||||
|
||||
### 1. Basitlik ve Açıklık
|
||||
|
||||
Go, zekiceden ziyade basitliği tercih eder. Kod açık ve okunması kolay olmalıdır.
|
||||
|
||||
```go
|
||||
// İyi: Açık ve doğrudan
|
||||
func GetUser(id string) (*User, error) {
|
||||
user, err := db.FindUser(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get user %s: %w", id, err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Kötü: Aşırı zeki
|
||||
func GetUser(id string) (*User, error) {
|
||||
return func() (*User, error) {
|
||||
if u, e := db.FindUser(id); e == nil {
|
||||
return u, nil
|
||||
} else {
|
||||
return nil, e
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Sıfır Değeri Kullanışlı Yapın
|
||||
|
||||
Türleri, sıfır değerinin başlatma olmadan hemen kullanılabilir olacağı şekilde tasarlayın.
|
||||
|
||||
```go
|
||||
// İyi: Sıfır değer kullanışlıdır
|
||||
type Counter struct {
|
||||
mu sync.Mutex
|
||||
count int // sıfır değer 0'dır, kullanıma hazırdır
|
||||
}
|
||||
|
||||
func (c *Counter) Inc() {
|
||||
c.mu.Lock()
|
||||
c.count++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// İyi: bytes.Buffer sıfır değerle çalışır
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("hello")
|
||||
|
||||
// Kötü: Başlatma gerektirir
|
||||
type BadCounter struct {
|
||||
counts map[string]int // nil map panic verir
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Interface Kabul Et, Struct Döndür
|
||||
|
||||
Fonksiyonlar interface parametreleri kabul etmeli ve somut tipler döndürmelidir.
|
||||
|
||||
```go
|
||||
// İyi: Interface kabul eder, somut tip döndürür
|
||||
func ProcessData(r io.Reader) (*Result, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{Data: data}, nil
|
||||
}
|
||||
|
||||
// Kötü: Interface döndürür (implementasyon detaylarını gereksiz yere gizler)
|
||||
func ProcessData(r io.Reader) (io.Reader, error) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Hata İşleme Desenleri
|
||||
|
||||
### Bağlam ile Hata Sarmalama
|
||||
|
||||
```go
|
||||
// İyi: Hataları bağlamla sarmalayın
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load config %s: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Özel Hata Tipleri
|
||||
|
||||
```go
|
||||
// Domain'e özgü hataları tanımlayın
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// Yaygın durumlar için sentinel hatalar
|
||||
var (
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
)
|
||||
```
|
||||
|
||||
### errors.Is ve errors.As ile Hata Kontrolü
|
||||
|
||||
```go
|
||||
func HandleError(err error) {
|
||||
// Belirli bir hatayı kontrol et
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
log.Println("No records found")
|
||||
return
|
||||
}
|
||||
|
||||
// Hata tipini kontrol et
|
||||
var validationErr *ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
log.Printf("Validation error on field %s: %s",
|
||||
validationErr.Field, validationErr.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// Bilinmeyen hata
|
||||
log.Printf("Unexpected error: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Hataları Asla Göz Ardı Etmeyin
|
||||
|
||||
```go
|
||||
// Kötü: Boş tanımlayıcı ile hatayı göz ardı etmek
|
||||
result, _ := doSomething()
|
||||
|
||||
// İyi: Hatayı işleyin veya neden göz ardı edildiğini açıkça belgelendirin
|
||||
result, err := doSomething()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Kabul edilebilir: Hata gerçekten önemli olmadığında (nadir)
|
||||
_ = writer.Close() // En iyi çaba temizliği, hata başka yerde loglanır
|
||||
```
|
||||
|
||||
## Eşzamanlılık Desenleri
|
||||
|
||||
### Worker Pool
|
||||
|
||||
```go
|
||||
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for job := range jobs {
|
||||
results <- process(job)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}
|
||||
```
|
||||
|
||||
### İptal ve Zaman Aşımları için Context
|
||||
|
||||
```go
|
||||
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
```
|
||||
|
||||
### Zarif Kapatma
|
||||
|
||||
```go
|
||||
func GracefulShutdown(server *http.Server) {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Server exited")
|
||||
}
|
||||
```
|
||||
|
||||
### Koordineli Goroutine'ler için errgroup
|
||||
|
||||
```go
|
||||
import "golang.org/x/sync/errgroup"
|
||||
|
||||
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
results := make([][]byte, len(urls))
|
||||
|
||||
for i, url := range urls {
|
||||
i, url := i, url // Loop değişkenlerini yakala
|
||||
g.Go(func() error {
|
||||
data, err := FetchWithTimeout(ctx, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results[i] = data
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Goroutine Sızıntılarından Kaçınma
|
||||
|
||||
```go
|
||||
// Kötü: Context iptal edilirse goroutine sızıntısı
|
||||
func leakyFetch(ctx context.Context, url string) <-chan []byte {
|
||||
ch := make(chan []byte)
|
||||
go func() {
|
||||
data, _ := fetch(url)
|
||||
ch <- data // Alıcı yoksa sonsuza kadar bloklar
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// İyi: İptali düzgün bir şekilde işler
|
||||
func safeFetch(ctx context.Context, url string) <-chan []byte {
|
||||
ch := make(chan []byte, 1) // Tamponlu kanal
|
||||
go func() {
|
||||
data, err := fetch(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- data:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
```
|
||||
|
||||
## Interface Tasarımı
|
||||
|
||||
### Küçük, Odaklanmış Interface'ler
|
||||
|
||||
```go
|
||||
// İyi: Tek metodlu interface'ler
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type Closer interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Interface'leri gerektiği gibi birleştirin
|
||||
type ReadWriteCloser interface {
|
||||
Reader
|
||||
Writer
|
||||
Closer
|
||||
}
|
||||
```
|
||||
|
||||
### Interface'leri Kullanıldıkları Yerde Tanımlayın
|
||||
|
||||
```go
|
||||
// Sağlayıcı pakette değil, tüketici pakette
|
||||
package service
|
||||
|
||||
// UserStore bu servisin neye ihtiyacı olduğunu tanımlar
|
||||
type UserStore interface {
|
||||
GetUser(id string) (*User, error)
|
||||
SaveUser(user *User) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
store UserStore
|
||||
}
|
||||
|
||||
// Somut implementasyon başka bir pakette olabilir
|
||||
// Bu interface'i bilmesine gerek yoktur
|
||||
```
|
||||
|
||||
### Type Assertion ile Opsiyonel Davranış
|
||||
|
||||
```go
|
||||
type Flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
func WriteAndFlush(w io.Writer, data []byte) error {
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Destekleniyorsa flush et
|
||||
if f, ok := w.(Flusher); ok {
|
||||
return f.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Paket Organizasyonu
|
||||
|
||||
### Standart Proje Düzeni
|
||||
|
||||
```text
|
||||
myproject/
|
||||
├── cmd/
|
||||
│ └── myapp/
|
||||
│ └── main.go # Giriş noktası
|
||||
├── internal/
|
||||
│ ├── handler/ # HTTP handler'lar
|
||||
│ ├── service/ # İş mantığı
|
||||
│ ├── repository/ # Veri erişimi
|
||||
│ └── config/ # Yapılandırma
|
||||
├── pkg/
|
||||
│ └── client/ # Public API client
|
||||
├── api/
|
||||
│ └── v1/ # API tanımları (proto, OpenAPI)
|
||||
├── testdata/ # Test fixture'ları
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
### Paket İsimlendirme
|
||||
|
||||
```go
|
||||
// İyi: Kısa, küçük harf, alt çizgi yok
|
||||
package http
|
||||
package json
|
||||
package user
|
||||
|
||||
// Kötü: Verbose, karışık büyük/küçük harf veya gereksiz
|
||||
package httpHandler
|
||||
package json_parser
|
||||
package userService // Gereksiz 'Service' eki
|
||||
```
|
||||
|
||||
### Paket Seviyesi State'ten Kaçının
|
||||
|
||||
```go
|
||||
// Kötü: Global değişken state
|
||||
var db *sql.DB
|
||||
|
||||
func init() {
|
||||
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
||||
}
|
||||
|
||||
// İyi: Dependency injection
|
||||
type Server struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewServer(db *sql.DB) *Server {
|
||||
return &Server{db: db}
|
||||
}
|
||||
```
|
||||
|
||||
## Struct Tasarımı
|
||||
|
||||
### Functional Options Deseni
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
addr string
|
||||
timeout time.Duration
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
type Option func(*Server)
|
||||
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(s *Server) {
|
||||
s.timeout = d
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(l *log.Logger) Option {
|
||||
return func(s *Server) {
|
||||
s.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(addr string, opts ...Option) *Server {
|
||||
s := &Server{
|
||||
addr: addr,
|
||||
timeout: 30 * time.Second, // varsayılan
|
||||
logger: log.Default(), // varsayılan
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
server := NewServer(":8080",
|
||||
WithTimeout(60*time.Second),
|
||||
WithLogger(customLogger),
|
||||
)
|
||||
```
|
||||
|
||||
### Kompozisyon için Embedding
|
||||
|
||||
```go
|
||||
type Logger struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (l *Logger) Log(msg string) {
|
||||
fmt.Printf("[%s] %s\n", l.prefix, msg)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
*Logger // Embedding - Server Log metodunu alır
|
||||
addr string
|
||||
}
|
||||
|
||||
func NewServer(addr string) *Server {
|
||||
return &Server{
|
||||
Logger: &Logger{prefix: "SERVER"},
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
s := NewServer(":8080")
|
||||
s.Log("Starting...") // Gömülü Logger.Log'u çağırır
|
||||
```
|
||||
|
||||
## Bellek ve Performans
|
||||
|
||||
### Boyut Bilindiğinde Slice'ları Önceden Tahsis Edin
|
||||
|
||||
```go
|
||||
// Kötü: Slice'ı birden çok kez büyütür
|
||||
func processItems(items []Item) []Result {
|
||||
var results []Result
|
||||
for _, item := range items {
|
||||
results = append(results, process(item))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// İyi: Tek tahsis
|
||||
func processItems(items []Item) []Result {
|
||||
results := make([]Result, 0, len(items))
|
||||
for _, item := range items {
|
||||
results = append(results, process(item))
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
### Sık Tahsisler için sync.Pool Kullanın
|
||||
|
||||
```go
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func ProcessRequest(data []byte) []byte {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}()
|
||||
|
||||
buf.Write(data)
|
||||
// İşle...
|
||||
return buf.Bytes()
|
||||
}
|
||||
```
|
||||
|
||||
### Döngülerde String Birleştirmekten Kaçının
|
||||
|
||||
```go
|
||||
// Kötü: Birçok string tahsisi oluşturur
|
||||
func join(parts []string) string {
|
||||
var result string
|
||||
for _, p := range parts {
|
||||
result += p + ","
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// İyi: strings.Builder ile tek tahsis
|
||||
func join(parts []string) string {
|
||||
var sb strings.Builder
|
||||
for i, p := range parts {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(p)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// En iyi: Standart kütüphaneyi kullanın
|
||||
func join(parts []string) string {
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
```
|
||||
|
||||
## Go Tooling Entegrasyonu
|
||||
|
||||
### Temel Komutlar
|
||||
|
||||
```bash
|
||||
# Build ve çalıştır
|
||||
go build ./...
|
||||
go run ./cmd/myapp
|
||||
|
||||
# Test
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
go test -cover ./...
|
||||
|
||||
# Statik analiz
|
||||
go vet ./...
|
||||
staticcheck ./...
|
||||
golangci-lint run
|
||||
|
||||
# Modül yönetimi
|
||||
go mod tidy
|
||||
go mod verify
|
||||
|
||||
# Formatlama
|
||||
gofmt -w .
|
||||
goimports -w .
|
||||
```
|
||||
|
||||
### Önerilen Linter Yapılandırması (.golangci.yml)
|
||||
|
||||
```yaml
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- gofmt
|
||||
- goimports
|
||||
- misspell
|
||||
- unconvert
|
||||
- unparam
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
govet:
|
||||
check-shadowing: true
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
```
|
||||
|
||||
## Hızlı Referans: Go İfadeleri
|
||||
|
||||
| İfade | Açıklama |
|
||||
|-------|----------|
|
||||
| Interface kabul et, struct döndür | Fonksiyonlar interface parametreleri kabul eder, somut tipler döndürür |
|
||||
| Hatalar değerdir | Hataları exception değil birinci sınıf değerler olarak ele alın |
|
||||
| Belleği paylaşarak iletişim kurmayın | Goroutine'ler arası koordinasyon için kanalları kullanın |
|
||||
| Sıfır değeri kullanışlı yapın | Tipler açık başlatma olmadan çalışmalıdır |
|
||||
| Biraz kopyalama biraz bağımlılıktan iyidir | Gereksiz dış bağımlılıklardan kaçının |
|
||||
| Açık zekiden iyidir | Okunabilirliği zekiceden öncelikli kılın |
|
||||
| gofmt kimsenin favorisi değil ama herkesin arkadaşı | Her zaman gofmt/goimports ile formatlayın |
|
||||
| Erken dönün | Hataları önce işleyin, mutlu yolu girintilendirilmemiş tutun |
|
||||
|
||||
## Kaçınılması Gereken Anti-Desenler
|
||||
|
||||
```go
|
||||
// Kötü: Uzun fonksiyonlarda naked return'ler
|
||||
func process() (result int, err error) {
|
||||
// ... 50 satır ...
|
||||
return // Ne döndürülüyor?
|
||||
}
|
||||
|
||||
// Kötü: Kontrol akışı için panic kullanmak
|
||||
func GetUser(id string) *User {
|
||||
user, err := db.Find(id)
|
||||
if err != nil {
|
||||
panic(err) // Bunu yapmayın
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// Kötü: Struct içinde context geçmek
|
||||
type Request struct {
|
||||
ctx context.Context // Context ilk parametre olmalı
|
||||
ID string
|
||||
}
|
||||
|
||||
// İyi: Context ilk parametre olarak
|
||||
func ProcessRequest(ctx context.Context, id string) error {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Kötü: Value ve pointer receiver'ları karıştırmak
|
||||
type Counter struct{ n int }
|
||||
func (c Counter) Value() int { return c.n } // Value receiver
|
||||
func (c *Counter) Increment() { c.n++ } // Pointer receiver
|
||||
// Bir stil seçin ve tutarlı olun
|
||||
```
|
||||
|
||||
**Unutmayın**: Go kodu en iyi anlamda sıkıcı olmalıdır - öngörülebilir, tutarlı ve anlaşılması kolay. Şüphe duyduğunuzda, basit tutun.
|
||||
720
docs/tr/skills/golang-testing/SKILL.md
Normal file
720
docs/tr/skills/golang-testing/SKILL.md
Normal file
@@ -0,0 +1,720 @@
|
||||
---
|
||||
name: golang-testing
|
||||
description: Table-driven testler, subtestler, benchmark'lar, fuzzing ve test coverage içeren Go test desenleri. TDD metodolojisi ile idiomatic Go uygulamalarını takip eder.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Go Test Desenleri
|
||||
|
||||
TDD metodolojisini takip eden güvenilir, bakımı kolay testler yazmak için kapsamlı Go test desenleri.
|
||||
|
||||
## Ne Zaman Etkinleştirmeli
|
||||
|
||||
- Yeni Go fonksiyonları veya metodları yazarken
|
||||
- Mevcut koda test coverage eklerken
|
||||
- Performans-kritik kod için benchmark'lar oluştururken
|
||||
- Input validation için fuzz testler implement ederken
|
||||
- Go projelerinde TDD workflow'u takip ederken
|
||||
|
||||
## Go için TDD Workflow'u
|
||||
|
||||
### RED-GREEN-REFACTOR Döngüsü
|
||||
|
||||
```
|
||||
RED → Önce başarısız bir test yaz
|
||||
GREEN → Testi geçirmek için minimal kod yaz
|
||||
REFACTOR → Testleri yeşil tutarken kodu iyileştir
|
||||
REPEAT → Sonraki gereksinimle devam et
|
||||
```
|
||||
|
||||
### Go'da Adım Adım TDD
|
||||
|
||||
```go
|
||||
// Adım 1: Interface/signature'ı tanımla
|
||||
// calculator.go
|
||||
package calculator
|
||||
|
||||
func Add(a, b int) int {
|
||||
panic("not implemented") // Placeholder
|
||||
}
|
||||
|
||||
// Adım 2: Başarısız test yaz (RED)
|
||||
// calculator_test.go
|
||||
package calculator
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
got := Add(2, 3)
|
||||
want := 5
|
||||
if got != want {
|
||||
t.Errorf("Add(2, 3) = %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Adım 3: Testi çalıştır - FAIL'i doğrula
|
||||
// $ go test
|
||||
// --- FAIL: TestAdd (0.00s)
|
||||
// panic: not implemented
|
||||
|
||||
// Adım 4: Minimal kodu implement et (GREEN)
|
||||
func Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// Adım 5: Testi çalıştır - PASS'i doğrula
|
||||
// $ go test
|
||||
// PASS
|
||||
|
||||
// Adım 6: Gerekirse refactor et, testlerin hala geçtiğini doğrula
|
||||
```
|
||||
|
||||
## Table-Driven Testler
|
||||
|
||||
Go testleri için standart desen. Minimal kodla kapsamlı coverage sağlar.
|
||||
|
||||
```go
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b int
|
||||
expected int
|
||||
}{
|
||||
{"positive numbers", 2, 3, 5},
|
||||
{"negative numbers", -1, -2, -3},
|
||||
{"zero values", 0, 0, 0},
|
||||
{"mixed signs", -1, 1, 0},
|
||||
{"large numbers", 1000000, 2000000, 3000000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Add(tt.a, tt.b)
|
||||
if got != tt.expected {
|
||||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||
tt.a, tt.b, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hata Durumları ile Table-Driven Testler
|
||||
|
||||
```go
|
||||
func TestParseConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
input: `{"host": "localhost", "port": 8080}`,
|
||||
want: &Config{Host: "localhost", Port: 8080},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
input: `{invalid}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "minimal config",
|
||||
input: `{}`,
|
||||
want: &Config{}, // Sıfır değer config
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseConfig(tt.input)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %+v; want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Subtestler ve Sub-benchmark'lar
|
||||
|
||||
### İlgili Testleri Organize Etme
|
||||
|
||||
```go
|
||||
func TestUser(t *testing.T) {
|
||||
// Tüm subtestler tarafından paylaşılan setup
|
||||
db := setupTestDB(t)
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
user := &User{Name: "Alice"}
|
||||
err := db.CreateUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser failed: %v", err)
|
||||
}
|
||||
if user.ID == "" {
|
||||
t.Error("expected user ID to be set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
user, err := db.GetUser("alice-id")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser failed: %v", err)
|
||||
}
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
// ...
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Paralel Subtestler
|
||||
|
||||
```go
|
||||
func TestParallel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"case1", "input1"},
|
||||
{"case2", "input2"},
|
||||
{"case3", "input3"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // Range değişkenini yakala
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel() // Subtestleri paralel çalıştır
|
||||
result := Process(tt.input)
|
||||
// assertion'lar...
|
||||
_ = result
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Helper'ları
|
||||
|
||||
### Helper Fonksiyonlar
|
||||
|
||||
```go
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper() // Bunu helper fonksiyon olarak işaretle
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
|
||||
// Test bittiğinde temizlik
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
// Migration'ları çalıştır
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
t.Fatalf("failed to create schema: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func assertNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual[T comparable](t *testing.T, got, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Geçici Dosyalar ve Dizinler
|
||||
|
||||
```go
|
||||
func TestFileProcessing(t *testing.T) {
|
||||
// Geçici dizin oluştur - otomatik olarak temizlenir
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test dosyası oluştur
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// Testi çalıştır
|
||||
result, err := ProcessFile(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Assert...
|
||||
_ = result
|
||||
}
|
||||
```
|
||||
|
||||
## Golden File'lar
|
||||
|
||||
`testdata/` içinde saklanan beklenen çıktı dosyalarına karşı test etme.
|
||||
|
||||
```go
|
||||
var update = flag.Bool("update", false, "update golden files")
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input Template
|
||||
}{
|
||||
{"simple", Template{Name: "test"}},
|
||||
{"complex", Template{Name: "test", Items: []string{"a", "b"}}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Render(tt.input)
|
||||
|
||||
golden := filepath.Join("testdata", tt.name+".golden")
|
||||
|
||||
if *update {
|
||||
// Golden dosyayı güncelle: go test -update
|
||||
err := os.WriteFile(golden, got, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to update golden file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
want, err := os.ReadFile(golden)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read golden file: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interface'ler ile Mocking
|
||||
|
||||
### Interface Tabanlı Mocking
|
||||
|
||||
```go
|
||||
// Bağımlılıklar için interface tanımlayın
|
||||
type UserRepository interface {
|
||||
GetUser(id string) (*User, error)
|
||||
SaveUser(user *User) error
|
||||
}
|
||||
|
||||
// Production implementasyonu
|
||||
type PostgresUserRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
|
||||
// Gerçek veritabanı sorgusu
|
||||
}
|
||||
|
||||
// Testler için mock implementasyon
|
||||
type MockUserRepository struct {
|
||||
GetUserFunc func(id string) (*User, error)
|
||||
SaveUserFunc func(user *User) error
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetUser(id string) (*User, error) {
|
||||
return m.GetUserFunc(id)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) SaveUser(user *User) error {
|
||||
return m.SaveUserFunc(user)
|
||||
}
|
||||
|
||||
// Mock kullanarak test
|
||||
func TestUserService(t *testing.T) {
|
||||
mock := &MockUserRepository{
|
||||
GetUserFunc: func(id string) (*User, error) {
|
||||
if id == "123" {
|
||||
return &User{ID: "123", Name: "Alice"}, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
service := NewUserService(mock)
|
||||
|
||||
user, err := service.GetUserProfile("123")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benchmark'lar
|
||||
|
||||
### Temel Benchmark'lar
|
||||
|
||||
```go
|
||||
func BenchmarkProcess(b *testing.B) {
|
||||
data := generateTestData(1000)
|
||||
b.ResetTimer() // Setup süresini sayma
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Process(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Çalıştır: go test -bench=BenchmarkProcess -benchmem
|
||||
// Çıktı: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op
|
||||
```
|
||||
|
||||
### Farklı Boyutlarla Benchmark
|
||||
|
||||
```go
|
||||
func BenchmarkSort(b *testing.B) {
|
||||
sizes := []int{100, 1000, 10000, 100000}
|
||||
|
||||
for _, size := range sizes {
|
||||
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
||||
data := generateRandomSlice(size)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Zaten sıralanmış veriyi sıralamaktan kaçınmak için kopya oluştur
|
||||
tmp := make([]int, len(data))
|
||||
copy(tmp, data)
|
||||
sort.Ints(tmp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bellek Tahsis Benchmark'ları
|
||||
|
||||
```go
|
||||
func BenchmarkStringConcat(b *testing.B) {
|
||||
parts := []string{"hello", "world", "foo", "bar", "baz"}
|
||||
|
||||
b.Run("plus", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var s string
|
||||
for _, p := range parts {
|
||||
s += p
|
||||
}
|
||||
_ = s
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("builder", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var sb strings.Builder
|
||||
for _, p := range parts {
|
||||
sb.WriteString(p)
|
||||
}
|
||||
_ = sb.String()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("join", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.Join(parts, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Fuzzing (Go 1.18+)
|
||||
|
||||
### Temel Fuzz Testi
|
||||
|
||||
```go
|
||||
func FuzzParseJSON(f *testing.F) {
|
||||
// Seed corpus ekle
|
||||
f.Add(`{"name": "test"}`)
|
||||
f.Add(`{"count": 123}`)
|
||||
f.Add(`[]`)
|
||||
f.Add(`""`)
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal([]byte(input), &result)
|
||||
|
||||
if err != nil {
|
||||
// Rastgele input için geçersiz JSON beklenebilir
|
||||
return
|
||||
}
|
||||
|
||||
// Parsing başarılıysa, yeniden encoding çalışmalı
|
||||
_, err = json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Errorf("Marshal failed after successful Unmarshal: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Çalıştır: go test -fuzz=FuzzParseJSON -fuzztime=30s
|
||||
```
|
||||
|
||||
### Birden Çok Input ile Fuzz Testi
|
||||
|
||||
```go
|
||||
func FuzzCompare(f *testing.F) {
|
||||
f.Add("hello", "world")
|
||||
f.Add("", "")
|
||||
f.Add("abc", "abc")
|
||||
|
||||
f.Fuzz(func(t *testing.T, a, b string) {
|
||||
result := Compare(a, b)
|
||||
|
||||
// Özellik: Compare(a, a) her zaman 0'a eşit olmalı
|
||||
if a == b && result != 0 {
|
||||
t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
|
||||
}
|
||||
|
||||
// Özellik: Compare(a, b) ve Compare(b, a) zıt işarete sahip olmalı
|
||||
reverse := Compare(b, a)
|
||||
if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
|
||||
if result != 0 || reverse != 0 {
|
||||
t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
|
||||
a, b, result, b, a, reverse)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Coverage Çalıştırma
|
||||
|
||||
```bash
|
||||
# Temel coverage
|
||||
go test -cover ./...
|
||||
|
||||
# Coverage profili oluştur
|
||||
go test -coverprofile=coverage.out ./...
|
||||
|
||||
# Coverage'ı tarayıcıda görüntüle
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Fonksiyona göre coverage görüntüle
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
# Race detection ile coverage
|
||||
go test -race -coverprofile=coverage.out ./...
|
||||
```
|
||||
|
||||
### Coverage Hedefleri
|
||||
|
||||
| Kod Tipi | Hedef |
|
||||
|----------|-------|
|
||||
| Kritik iş mantığı | 100% |
|
||||
| Public API'ler | 90%+ |
|
||||
| Genel kod | 80%+ |
|
||||
| Oluşturulan kod | Hariç tut |
|
||||
|
||||
### Oluşturulan Kodu Coverage'dan Hariç Tutma
|
||||
|
||||
```go
|
||||
//go:generate mockgen -source=interface.go -destination=mock_interface.go
|
||||
|
||||
// Coverage profile'ında, build tag'leri ile hariç tut:
|
||||
// go test -cover -tags=!generate ./...
|
||||
```
|
||||
|
||||
## HTTP Handler Testleri
|
||||
|
||||
```go
|
||||
func TestHealthHandler(t *testing.T) {
|
||||
// Request oluştur
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Handler'ı çağır
|
||||
HealthHandler(w, req)
|
||||
|
||||
// Response'u kontrol et
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != "OK" {
|
||||
t.Errorf("got body %q; want %q", body, "OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
wantStatus int
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "get user",
|
||||
method: http.MethodGet,
|
||||
path: "/users/123",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"id":"123","name":"Alice"}`,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
method: http.MethodGet,
|
||||
path: "/users/999",
|
||||
wantStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "create user",
|
||||
method: http.MethodPost,
|
||||
path: "/users",
|
||||
body: `{"name":"Bob"}`,
|
||||
wantStatus: http.StatusCreated,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAPIHandler()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var body io.Reader
|
||||
if tt.body != "" {
|
||||
body = strings.NewReader(tt.body)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tt.method, tt.path, body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != tt.wantStatus {
|
||||
t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
|
||||
}
|
||||
|
||||
if tt.wantBody != "" && w.Body.String() != tt.wantBody {
|
||||
t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Komutları
|
||||
|
||||
```bash
|
||||
# Tüm testleri çalıştır
|
||||
go test ./...
|
||||
|
||||
# Verbose çıktı ile testleri çalıştır
|
||||
go test -v ./...
|
||||
|
||||
# Belirli bir testi çalıştır
|
||||
go test -run TestAdd ./...
|
||||
|
||||
# Pattern ile eşleşen testleri çalıştır
|
||||
go test -run "TestUser/Create" ./...
|
||||
|
||||
# Race detector ile testleri çalıştır
|
||||
go test -race ./...
|
||||
|
||||
# Coverage ile testleri çalıştır
|
||||
go test -cover -coverprofile=coverage.out ./...
|
||||
|
||||
# Sadece kısa testleri çalıştır
|
||||
go test -short ./...
|
||||
|
||||
# Timeout ile testleri çalıştır
|
||||
go test -timeout 30s ./...
|
||||
|
||||
# Benchmark'ları çalıştır
|
||||
go test -bench=. -benchmem ./...
|
||||
|
||||
# Fuzzing çalıştır
|
||||
go test -fuzz=FuzzParse -fuzztime=30s ./...
|
||||
|
||||
# Test çalışma sayısı (flaky test tespiti için)
|
||||
go test -count=10 ./...
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
**YAPIN:**
|
||||
- Testleri ÖNCE yazın (TDD)
|
||||
- Kapsamlı coverage için table-driven testler kullanın
|
||||
- İmplementasyon değil davranış test edin
|
||||
- Helper fonksiyonlarda `t.Helper()` kullanın
|
||||
- Bağımsız testler için `t.Parallel()` kullanın
|
||||
- Kaynakları `t.Cleanup()` ile temizleyin
|
||||
- Senaryoyu açıklayan anlamlı test isimleri kullanın
|
||||
|
||||
**YAPMAYIN:**
|
||||
- Private fonksiyonları doğrudan test etmeyin (public API üzerinden test edin)
|
||||
- Testlerde `time.Sleep()` kullanmayın (channel'lar veya condition'lar kullanın)
|
||||
- Flaky testleri göz ardı etmeyin (düzeltin veya kaldırın)
|
||||
- Her şeyi mocklamayın (mümkün olduğunda integration testlerini tercih edin)
|
||||
- Hata yolu testini atlamayın
|
||||
|
||||
## CI/CD ile Entegrasyon
|
||||
|
||||
```yaml
|
||||
# GitHub Actions örneği
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Run tests
|
||||
run: go test -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Check coverage
|
||||
run: |
|
||||
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
|
||||
awk -F'%' '{if ($1 < 80) exit 1}'
|
||||
```
|
||||
|
||||
**Unutmayın**: Testler dokümantasyondur. Kodunuzun nasıl kullanılması gerektiğini gösterirler. Testleri açık yazın ve güncel tutun.
|
||||
151
docs/tr/skills/jpa-patterns/SKILL.md
Normal file
151
docs/tr/skills/jpa-patterns/SKILL.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: jpa-patterns
|
||||
description: Spring Boot'ta entity tasarımı, ilişkiler, sorgu optimizasyonu, transaction'lar, auditing, indeksleme, sayfalama ve pooling için JPA/Hibernate kalıpları.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# JPA/Hibernate Kalıpları
|
||||
|
||||
Spring Boot'ta veri modelleme, repository'ler ve performans ayarlaması için kullanın.
|
||||
|
||||
## Ne Zaman Aktifleştirmeli
|
||||
|
||||
- JPA entity'leri ve tablo eşlemelerini tasarlarken
|
||||
- İlişkileri tanımlarken (@OneToMany, @ManyToOne, @ManyToMany)
|
||||
- Sorguları optimize ederken (N+1 önleme, fetch stratejileri, projections)
|
||||
- Transaction'ları, auditing'i veya soft delete'leri yapılandırırken
|
||||
- Sayfalama, sıralama veya özel repository metodları kurarken
|
||||
- Connection pooling (HikariCP) veya second-level caching ayarlarken
|
||||
|
||||
## Entity Tasarımı
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "markets", indexes = {
|
||||
@Index(name = "idx_markets_slug", columnList = "slug", unique = true)
|
||||
})
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class MarketEntity {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 120)
|
||||
private String slug;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private MarketStatus status = MarketStatus.ACTIVE;
|
||||
|
||||
@CreatedDate private Instant createdAt;
|
||||
@LastModifiedDate private Instant updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
Auditing'i etkinleştir:
|
||||
```java
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
class JpaConfig {}
|
||||
```
|
||||
|
||||
## İlişkiler ve N+1 Önleme
|
||||
|
||||
```java
|
||||
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<PositionEntity> positions = new ArrayList<>();
|
||||
```
|
||||
|
||||
- Varsayılan olarak lazy loading; gerektiğinde sorgularda `JOIN FETCH` kullan
|
||||
- Koleksiyonlarda `EAGER` kullanmaktan kaçın; okuma yolları için DTO projections kullan
|
||||
|
||||
```java
|
||||
@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id")
|
||||
Optional<MarketEntity> findWithPositions(@Param("id") Long id);
|
||||
```
|
||||
|
||||
## Repository Kalıpları
|
||||
|
||||
```java
|
||||
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
|
||||
Optional<MarketEntity> findBySlug(String slug);
|
||||
|
||||
@Query("select m from MarketEntity m where m.status = :status")
|
||||
Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
|
||||
}
|
||||
```
|
||||
|
||||
- Hafif sorgular için projections kullan:
|
||||
```java
|
||||
public interface MarketSummary {
|
||||
Long getId();
|
||||
String getName();
|
||||
MarketStatus getStatus();
|
||||
}
|
||||
Page<MarketSummary> findAllBy(Pageable pageable);
|
||||
```
|
||||
|
||||
## Transaction'lar
|
||||
|
||||
- Servis metodlarını `@Transactional` ile işaretle
|
||||
- Okuma yollarını optimize etmek için `@Transactional(readOnly = true)` kullan
|
||||
- Propagation'ı dikkatle seç; uzun süreli transaction'lardan kaçın
|
||||
|
||||
```java
|
||||
@Transactional
|
||||
public Market updateStatus(Long id, MarketStatus status) {
|
||||
MarketEntity entity = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Market"));
|
||||
entity.setStatus(status);
|
||||
return Market.from(entity);
|
||||
}
|
||||
```
|
||||
|
||||
## Sayfalama
|
||||
|
||||
```java
|
||||
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
|
||||
Page<MarketEntity> markets = repo.findByStatus(MarketStatus.ACTIVE, page);
|
||||
```
|
||||
|
||||
Cursor benzeri sayfalama için, sıralama ile birlikte JPQL'de `id > :lastId` ekle.
|
||||
|
||||
## İndeksleme ve Performans
|
||||
|
||||
- Yaygın filtreler için indeksler ekle (`status`, `slug`, foreign key'ler)
|
||||
- Sorgu kalıplarına uyan composite indeksler kullan (`status, created_at`)
|
||||
- `select *` kullanmaktan kaçın; sadece gerekli sütunları project et
|
||||
- `saveAll` ve `hibernate.jdbc.batch_size` ile yazmaları batch'le
|
||||
|
||||
## Connection Pooling (HikariCP)
|
||||
|
||||
Önerilen özellikler:
|
||||
```
|
||||
spring.datasource.hikari.maximum-pool-size=20
|
||||
spring.datasource.hikari.minimum-idle=5
|
||||
spring.datasource.hikari.connection-timeout=30000
|
||||
spring.datasource.hikari.validation-timeout=5000
|
||||
```
|
||||
|
||||
PostgreSQL LOB işleme için ekle:
|
||||
```
|
||||
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
- 1st-level cache EntityManager başına; transaction'lar arası entity'leri tutmaktan kaçın
|
||||
- Okuma ağırlıklı entity'ler için second-level cache'i dikkatle düşün; eviction stratejisini doğrula
|
||||
|
||||
## Migration'lar
|
||||
|
||||
- Flyway veya Liquibase kullan; üretimde Hibernate auto DDL'ye asla güvenme
|
||||
- Migration'ları idempotent ve ekleyici tut; plan olmadan sütun kaldırmaktan kaçın
|
||||
|
||||
## Veri Erişimi Testi
|
||||
|
||||
- Üretimi yansıtmak için Testcontainers ile `@DataJpaTest` tercih et
|
||||
- Logları kullanarak SQL verimliliğini assert et: parametre değerleri için `logging.level.org.hibernate.SQL=DEBUG` ve `logging.level.org.hibernate.orm.jdbc.bind=TRACE` ayarla
|
||||
|
||||
**Hatırla**: Entity'leri yalın, sorguları kasıtlı ve transaction'ları kısa tut. Fetch stratejileri ve projections ile N+1'i önle, ve okuma/yazma yolların için indeksle.
|
||||
535
docs/tr/skills/kotlin-patterns/SKILL.md
Normal file
535
docs/tr/skills/kotlin-patterns/SKILL.md
Normal file
@@ -0,0 +1,535 @@
|
||||
---
|
||||
name: kotlin-patterns
|
||||
description: Coroutine'ler, null safety ve DSL builder'lar ile sağlam, verimli ve sürdürülebilir Kotlin uygulamaları oluşturmak için idiomatic Kotlin kalıpları, en iyi uygulamalar ve konvansiyonlar.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Kotlin Geliştirme Kalıpları
|
||||
|
||||
Sağlam, verimli ve sürdürülebilir uygulamalar oluşturmak için idiomatic Kotlin kalıpları ve en iyi uygulamalar.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Yeni Kotlin kodu yazarken
|
||||
- Kotlin kodunu incelerken
|
||||
- Mevcut Kotlin kodunu refactor ederken
|
||||
- Kotlin modülleri veya kütüphaneleri tasarlarken
|
||||
- Gradle Kotlin DSL build'lerini yapılandırırken
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
Bu skill yedi temel alanda idiomatic Kotlin konvansiyonlarını uygular: tip sistemi ve safe-call operatörleri kullanarak null safety, `val` ve data class'larda `copy()` ile immutability, exhaustive tip hiyerarşileri için sealed class'lar ve interface'ler, coroutine'ler ve `Flow` ile yapılandırılmış eşzamanlılık, inheritance olmadan davranış eklemek için extension fonksiyonlar, `@DslMarker` ve lambda receiver'lar kullanarak tip güvenli DSL builder'lar, ve build yapılandırması için Gradle Kotlin DSL.
|
||||
|
||||
## Örnekler
|
||||
|
||||
**Elvis operatörü ile null safety:**
|
||||
```kotlin
|
||||
fun getUserEmail(userId: String): String {
|
||||
val user = userRepository.findById(userId)
|
||||
return user?.email ?: "unknown@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Exhaustive sonuçlar için sealed class:**
|
||||
```kotlin
|
||||
sealed class Result<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>()
|
||||
data class Failure(val error: AppError) : Result<Nothing>()
|
||||
data object Loading : Result<Nothing>()
|
||||
}
|
||||
```
|
||||
|
||||
**async/await ile yapılandırılmış eşzamanlılık:**
|
||||
```kotlin
|
||||
suspend fun fetchUserWithPosts(userId: String): UserProfile =
|
||||
coroutineScope {
|
||||
val user = async { userService.getUser(userId) }
|
||||
val posts = async { postService.getUserPosts(userId) }
|
||||
UserProfile(user = user.await(), posts = posts.await())
|
||||
}
|
||||
```
|
||||
|
||||
## Temel İlkeler
|
||||
|
||||
### 1. Null Safety
|
||||
|
||||
Kotlin'in tip sistemi nullable ve non-nullable tipleri ayırır. Tam olarak kullanın.
|
||||
|
||||
```kotlin
|
||||
// İyi: Varsayılan olarak non-nullable tipler kullan
|
||||
fun getUser(id: String): User {
|
||||
return userRepository.findById(id)
|
||||
?: throw UserNotFoundException("User $id not found")
|
||||
}
|
||||
|
||||
// İyi: Safe call'lar ve Elvis operatörü
|
||||
fun getUserEmail(userId: String): String {
|
||||
val user = userRepository.findById(userId)
|
||||
return user?.email ?: "unknown@example.com"
|
||||
}
|
||||
|
||||
// Kötü: Nullable tipleri zorla açma
|
||||
fun getUserEmail(userId: String): String {
|
||||
val user = userRepository.findById(userId)
|
||||
return user!!.email // null ise NPE fırlatır
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Varsayılan Olarak Immutability
|
||||
|
||||
`var` yerine `val` tercih edin, mutable koleksiyonlar yerine immutable olanları.
|
||||
|
||||
```kotlin
|
||||
// İyi: Immutable veri
|
||||
data class User(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val email: String,
|
||||
)
|
||||
|
||||
// İyi: copy() ile dönüştürme
|
||||
fun updateEmail(user: User, newEmail: String): User =
|
||||
user.copy(email = newEmail)
|
||||
|
||||
// İyi: Immutable koleksiyonlar
|
||||
val users: List<User> = listOf(user1, user2)
|
||||
val filtered = users.filter { it.email.isNotBlank() }
|
||||
|
||||
// Kötü: Mutable state
|
||||
var currentUser: User? = null // Mutable global state'ten kaçın
|
||||
val mutableUsers = mutableListOf<User>() // Gerçekten gerekmedikçe kaçın
|
||||
```
|
||||
|
||||
### 3. Expression Body'ler ve Tek İfadeli Fonksiyonlar
|
||||
|
||||
Kısa, okunabilir fonksiyonlar için expression body'ler kullanın.
|
||||
|
||||
```kotlin
|
||||
// İyi: Expression body
|
||||
fun isAdult(age: Int): Boolean = age >= 18
|
||||
|
||||
fun formatFullName(first: String, last: String): String =
|
||||
"$first $last".trim()
|
||||
|
||||
fun User.displayName(): String =
|
||||
name.ifBlank { email.substringBefore('@') }
|
||||
|
||||
// İyi: Expression olarak when
|
||||
fun statusMessage(code: Int): String = when (code) {
|
||||
200 -> "OK"
|
||||
404 -> "Not Found"
|
||||
500 -> "Internal Server Error"
|
||||
else -> "Unknown status: $code"
|
||||
}
|
||||
|
||||
// Kötü: Gereksiz block body
|
||||
fun isAdult(age: Int): Boolean {
|
||||
return age >= 18
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Value Objeler İçin Data Class'lar
|
||||
|
||||
Öncelikle veri tutan tipler için data class'lar kullanın.
|
||||
|
||||
```kotlin
|
||||
// İyi: copy, equals, hashCode, toString ile data class
|
||||
data class CreateUserRequest(
|
||||
val name: String,
|
||||
val email: String,
|
||||
val role: Role = Role.USER,
|
||||
)
|
||||
|
||||
// İyi: Tip güvenliği için value class (runtime'da sıfır maliyet)
|
||||
@JvmInline
|
||||
value class UserId(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "UserId cannot be blank" }
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class Email(val value: String) {
|
||||
init {
|
||||
require('@' in value) { "Invalid email: $value" }
|
||||
}
|
||||
}
|
||||
|
||||
fun getUser(id: UserId): User = userRepository.findById(id)
|
||||
```
|
||||
|
||||
## Sealed Class'lar ve Interface'ler
|
||||
|
||||
### Kısıtlı Hiyerarşileri Modelleme
|
||||
|
||||
```kotlin
|
||||
// İyi: Exhaustive when için sealed class
|
||||
sealed class Result<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>()
|
||||
data class Failure(val error: AppError) : Result<Nothing>()
|
||||
data object Loading : Result<Nothing>()
|
||||
}
|
||||
|
||||
fun <T> Result<T>.getOrNull(): T? = when (this) {
|
||||
is Result.Success -> data
|
||||
is Result.Failure -> null
|
||||
is Result.Loading -> null
|
||||
}
|
||||
|
||||
fun <T> Result<T>.getOrThrow(): T = when (this) {
|
||||
is Result.Success -> data
|
||||
is Result.Failure -> throw error.toException()
|
||||
is Result.Loading -> throw IllegalStateException("Still loading")
|
||||
}
|
||||
```
|
||||
|
||||
### API Yanıtları İçin Sealed Interface'ler
|
||||
|
||||
```kotlin
|
||||
sealed interface ApiError {
|
||||
val message: String
|
||||
|
||||
data class NotFound(override val message: String) : ApiError
|
||||
data class Unauthorized(override val message: String) : ApiError
|
||||
data class Validation(
|
||||
override val message: String,
|
||||
val field: String,
|
||||
) : ApiError
|
||||
data class Internal(
|
||||
override val message: String,
|
||||
val cause: Throwable? = null,
|
||||
) : ApiError
|
||||
}
|
||||
|
||||
fun ApiError.toStatusCode(): Int = when (this) {
|
||||
is ApiError.NotFound -> 404
|
||||
is ApiError.Unauthorized -> 401
|
||||
is ApiError.Validation -> 422
|
||||
is ApiError.Internal -> 500
|
||||
}
|
||||
```
|
||||
|
||||
## Scope Fonksiyonlar
|
||||
|
||||
### Her Birini Ne Zaman Kullanmalı
|
||||
|
||||
```kotlin
|
||||
// let: Nullable'ı veya scope edilmiş sonucu dönüştür
|
||||
val length: Int? = name?.let { it.trim().length }
|
||||
|
||||
// apply: Bir nesneyi yapılandır (nesneyi döndürür)
|
||||
val user = User().apply {
|
||||
name = "Alice"
|
||||
email = "alice@example.com"
|
||||
}
|
||||
|
||||
// also: Yan etkiler (nesneyi döndürür)
|
||||
val user = createUser(request).also { logger.info("Created user: ${it.id}") }
|
||||
|
||||
// run: Receiver ile block çalıştır (sonucu döndürür)
|
||||
val result = connection.run {
|
||||
prepareStatement(sql)
|
||||
executeQuery()
|
||||
}
|
||||
|
||||
// with: run'ın extension olmayan formu
|
||||
val csv = with(StringBuilder()) {
|
||||
appendLine("name,email")
|
||||
users.forEach { appendLine("${it.name},${it.email}") }
|
||||
toString()
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Fonksiyonlar
|
||||
|
||||
### Inheritance Olmadan Fonksiyonalite Ekleme
|
||||
|
||||
```kotlin
|
||||
// İyi: Domain'e özgü extension'lar
|
||||
fun String.toSlug(): String =
|
||||
lowercase()
|
||||
.replace(Regex("[^a-z0-9\\s-]"), "")
|
||||
.replace(Regex("\\s+"), "-")
|
||||
.trim('-')
|
||||
|
||||
fun Instant.toLocalDate(zone: ZoneId = ZoneId.systemDefault()): LocalDate =
|
||||
atZone(zone).toLocalDate()
|
||||
|
||||
// İyi: Koleksiyon extension'ları
|
||||
fun <T> List<T>.second(): T = this[1]
|
||||
|
||||
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
|
||||
|
||||
// İyi: Scope edilmiş extension'lar (global namespace'i kirletmez)
|
||||
class UserService {
|
||||
private fun User.isActive(): Boolean =
|
||||
status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS))
|
||||
|
||||
fun getActiveUsers(): List<User> = userRepository.findAll().filter { it.isActive() }
|
||||
}
|
||||
```
|
||||
|
||||
## Coroutine'ler
|
||||
|
||||
### Yapılandırılmış Eşzamanlılık
|
||||
|
||||
```kotlin
|
||||
// İyi: coroutineScope ile yapılandırılmış eşzamanlılık
|
||||
suspend fun fetchUserWithPosts(userId: String): UserProfile =
|
||||
coroutineScope {
|
||||
val userDeferred = async { userService.getUser(userId) }
|
||||
val postsDeferred = async { postService.getUserPosts(userId) }
|
||||
|
||||
UserProfile(
|
||||
user = userDeferred.await(),
|
||||
posts = postsDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
// İyi: child'lar bağımsız başarısız olabildiğinde supervisorScope
|
||||
suspend fun fetchDashboard(userId: String): Dashboard =
|
||||
supervisorScope {
|
||||
val user = async { userService.getUser(userId) }
|
||||
val notifications = async { notificationService.getRecent(userId) }
|
||||
val recommendations = async { recommendationService.getFor(userId) }
|
||||
|
||||
Dashboard(
|
||||
user = user.await(),
|
||||
notifications = try {
|
||||
notifications.await()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
},
|
||||
recommendations = try {
|
||||
recommendations.await()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Reactive Stream'ler İçin Flow
|
||||
|
||||
```kotlin
|
||||
// İyi: Uygun hata işleme ile cold flow
|
||||
fun observeUsers(): Flow<List<User>> = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
val users = userRepository.findAll()
|
||||
emit(users)
|
||||
delay(5.seconds)
|
||||
}
|
||||
}.catch { e ->
|
||||
logger.error("Error observing users", e)
|
||||
emit(emptyList())
|
||||
}
|
||||
|
||||
// İyi: Flow operatörleri
|
||||
fun searchUsers(query: Flow<String>): Flow<List<User>> =
|
||||
query
|
||||
.debounce(300.milliseconds)
|
||||
.distinctUntilChanged()
|
||||
.filter { it.length >= 2 }
|
||||
.mapLatest { q -> userRepository.search(q) }
|
||||
.catch { emit(emptyList()) }
|
||||
```
|
||||
|
||||
## DSL Builder'lar
|
||||
|
||||
### Tip Güvenli Builder'lar
|
||||
|
||||
```kotlin
|
||||
// İyi: @DslMarker ile DSL
|
||||
@DslMarker
|
||||
annotation class HtmlDsl
|
||||
|
||||
@HtmlDsl
|
||||
class HTML {
|
||||
private val children = mutableListOf<Element>()
|
||||
|
||||
fun head(init: Head.() -> Unit) {
|
||||
children += Head().apply(init)
|
||||
}
|
||||
|
||||
fun body(init: Body.() -> Unit) {
|
||||
children += Body().apply(init)
|
||||
}
|
||||
|
||||
override fun toString(): String = children.joinToString("\n")
|
||||
}
|
||||
|
||||
fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)
|
||||
|
||||
// Kullanım
|
||||
val page = html {
|
||||
head { title("My Page") }
|
||||
body {
|
||||
h1("Welcome")
|
||||
p("Hello, World!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gradle Kotlin DSL
|
||||
|
||||
### build.gradle.kts Yapılandırması
|
||||
|
||||
```kotlin
|
||||
// En son versiyonları kontrol et: https://kotlinlang.org/docs/releases.html
|
||||
plugins {
|
||||
kotlin("jvm") version "2.3.10"
|
||||
kotlin("plugin.serialization") version "2.3.10"
|
||||
id("io.ktor.plugin") version "3.4.0"
|
||||
id("org.jetbrains.kotlinx.kover") version "0.9.7"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.23.8"
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Ktor
|
||||
implementation("io.ktor:ktor-server-core:3.4.0")
|
||||
implementation("io.ktor:ktor-server-netty:3.4.0")
|
||||
implementation("io.ktor:ktor-server-content-negotiation:3.4.0")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.0")
|
||||
|
||||
// Exposed
|
||||
implementation("org.jetbrains.exposed:exposed-core:1.0.0")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:1.0.0")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0")
|
||||
implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0")
|
||||
|
||||
// Koin
|
||||
implementation("io.insert-koin:koin-ktor:4.2.0")
|
||||
|
||||
// Coroutines
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
|
||||
// Test
|
||||
testImplementation("io.kotest:kotest-runner-junit5:6.1.4")
|
||||
testImplementation("io.kotest:kotest-assertions-core:6.1.4")
|
||||
testImplementation("io.kotest:kotest-property:6.1.4")
|
||||
testImplementation("io.mockk:mockk:1.14.9")
|
||||
testImplementation("io.ktor:ktor-server-test-host:3.4.0")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
detekt {
|
||||
config.setFrom(files("config/detekt/detekt.yml"))
|
||||
buildUponDefaultConfig = true
|
||||
}
|
||||
```
|
||||
|
||||
## Hata İşleme Kalıpları
|
||||
|
||||
### Domain Operasyonları İçin Result Tipi
|
||||
|
||||
```kotlin
|
||||
// İyi: Kotlin'in Result'ını veya özel sealed class kullan
|
||||
suspend fun createUser(request: CreateUserRequest): Result<User> = runCatching {
|
||||
require(request.name.isNotBlank()) { "Name cannot be blank" }
|
||||
require('@' in request.email) { "Invalid email format" }
|
||||
|
||||
val user = User(
|
||||
id = UserId(UUID.randomUUID().toString()),
|
||||
name = request.name,
|
||||
email = Email(request.email),
|
||||
)
|
||||
userRepository.save(user)
|
||||
user
|
||||
}
|
||||
|
||||
// İyi: Result'ları zincirle
|
||||
val displayName = createUser(request)
|
||||
.map { it.name }
|
||||
.getOrElse { "Unknown" }
|
||||
```
|
||||
|
||||
### require, check, error
|
||||
|
||||
```kotlin
|
||||
// İyi: Net mesajlarla ön koşullar
|
||||
fun withdraw(account: Account, amount: Money): Account {
|
||||
require(amount.value > 0) { "Amount must be positive: $amount" }
|
||||
check(account.balance >= amount) { "Insufficient balance: ${account.balance} < $amount" }
|
||||
|
||||
return account.copy(balance = account.balance - amount)
|
||||
}
|
||||
```
|
||||
|
||||
## Hızlı Referans: Kotlin İdiyomları
|
||||
|
||||
| İdiyom | Açıklama |
|
||||
|-------|-------------|
|
||||
| `val` over `var` | Immutable değişkenleri tercih et |
|
||||
| `data class` | equals/hashCode/copy ile value objeler için |
|
||||
| `sealed class/interface` | Kısıtlı tip hiyerarşileri için |
|
||||
| `value class` | Sıfır maliyetli tip güvenli sarmalayıcılar için |
|
||||
| Expression `when` | Exhaustive pattern matching |
|
||||
| Safe call `?.` | Null-safe member erişimi |
|
||||
| Elvis `?:` | Nullable'lar için varsayılan değer |
|
||||
| `let`/`apply`/`also`/`run`/`with` | Temiz kod için scope fonksiyonlar |
|
||||
| Extension fonksiyonlar | Inheritance olmadan davranış ekle |
|
||||
| `copy()` | Data class'larda immutable güncellemeler |
|
||||
| `require`/`check` | Ön koşul assertion'ları |
|
||||
| Coroutine `async`/`await` | Yapılandırılmış concurrent execution |
|
||||
| `Flow` | Cold reactive stream'ler |
|
||||
| `sequence` | Lazy evaluation |
|
||||
| Delegation `by` | Inheritance olmadan implementasyonu yeniden kullan |
|
||||
|
||||
## Kaçınılması Gereken Anti-Kalıplar
|
||||
|
||||
```kotlin
|
||||
// Kötü: Nullable tipleri zorla açma
|
||||
val name = user!!.name
|
||||
|
||||
// Kötü: Java'dan platform tipi sızıntısı
|
||||
fun getLength(s: String) = s.length // Güvenli
|
||||
fun getLength(s: String?) = s?.length ?: 0 // Java'dan null'ları işle
|
||||
|
||||
// Kötü: Mutable data class'lar
|
||||
data class MutableUser(var name: String, var email: String)
|
||||
|
||||
// Kötü: Kontrol akışı için exception kullanma
|
||||
try {
|
||||
val user = findUser(id)
|
||||
} catch (e: NotFoundException) {
|
||||
// Beklenen durumlar için exception kullanma
|
||||
}
|
||||
|
||||
// İyi: Nullable dönüş veya Result kullan
|
||||
val user: User? = findUserOrNull(id)
|
||||
|
||||
// Kötü: Coroutine scope'u görmezden gelme
|
||||
GlobalScope.launch { /* GlobalScope'tan kaçın */ }
|
||||
|
||||
// İyi: Yapılandırılmış eşzamanlılık kullan
|
||||
coroutineScope {
|
||||
launch { /* Uygun şekilde scope edilmiş */ }
|
||||
}
|
||||
|
||||
// Kötü: Derin iç içe scope fonksiyonlar
|
||||
user?.let { u ->
|
||||
u.address?.let { a ->
|
||||
a.city?.let { c -> process(c) }
|
||||
}
|
||||
}
|
||||
|
||||
// İyi: Doğrudan null-safe zincir
|
||||
user?.address?.city?.let { process(it) }
|
||||
```
|
||||
|
||||
**Hatırla**: Kotlin kodu kısa ama okunabilir olmalı. Güvenlik için tip sisteminden yararlanın, immutability tercih edin ve eşzamanlılık için coroutine'ler kullanın. Şüpheye düştüğünüzde, derleyicinin size yardım etmesine izin verin.
|
||||
578
docs/tr/skills/kotlin-testing/SKILL.md
Normal file
578
docs/tr/skills/kotlin-testing/SKILL.md
Normal file
@@ -0,0 +1,578 @@
|
||||
---
|
||||
name: kotlin-testing
|
||||
description: Kotest, MockK, coroutine testi, property-based testing ve Kover coverage ile Kotlin test kalıpları. İdiomatic Kotlin uygulamalarıyla TDD metodolojisini takip eder.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Kotlin Test Kalıpları
|
||||
|
||||
Kotest ve MockK ile TDD metodolojisini takip ederek güvenilir, sürdürülebilir testler yazmak için kapsamlı Kotlin test kalıpları.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Yeni Kotlin fonksiyonları veya class'lar yazarken
|
||||
- Mevcut Kotlin koduna test coverage eklerken
|
||||
- Property-based testler uygularken
|
||||
- Kotlin projelerinde TDD iş akışını takip ederken
|
||||
- Kod coverage için Kover yapılandırırken
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
1. **Hedef kodu belirle** — Test edilecek fonksiyon, class veya modülü bul
|
||||
2. **Kotest spec yaz** — Test scope'una uygun bir spec stili seç (StringSpec, FunSpec, BehaviorSpec)
|
||||
3. **Bağımlılıkları mock'la** — Test edilen birimi izole etmek için MockK kullan
|
||||
4. **Testleri çalıştır (RED)** — Testin beklenen hatayla başarısız olduğunu doğrula
|
||||
5. **Kodu uygula (GREEN)** — Testi geçmek için minimal kod yaz
|
||||
6. **Refactor** — Testleri yeşil tutarken implementasyonu iyileştir
|
||||
7. **Coverage'ı kontrol et** — `./gradlew koverHtmlReport` çalıştır ve %80+ coverage'ı doğrula
|
||||
|
||||
## TDD İş Akışı for Kotlin
|
||||
|
||||
### RED-GREEN-REFACTOR Döngüsü
|
||||
|
||||
```
|
||||
RED -> Önce başarısız bir test yaz
|
||||
GREEN -> Testi geçmek için minimal kod yaz
|
||||
REFACTOR -> Testleri yeşil tutarken kodu iyileştir
|
||||
REPEAT -> Sonraki gereksinimle devam et
|
||||
```
|
||||
|
||||
### Kotlin'de Adım Adım TDD
|
||||
|
||||
```kotlin
|
||||
// Adım 1: Interface/signature tanımla
|
||||
// EmailValidator.kt
|
||||
package com.example.validator
|
||||
|
||||
fun validateEmail(email: String): Result<String> {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
// Adım 2: Başarısız test yaz (RED)
|
||||
// EmailValidatorTest.kt
|
||||
package com.example.validator
|
||||
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.result.shouldBeFailure
|
||||
import io.kotest.matchers.result.shouldBeSuccess
|
||||
|
||||
class EmailValidatorTest : StringSpec({
|
||||
"valid email returns success" {
|
||||
validateEmail("user@example.com").shouldBeSuccess("user@example.com")
|
||||
}
|
||||
|
||||
"empty email returns failure" {
|
||||
validateEmail("").shouldBeFailure()
|
||||
}
|
||||
|
||||
"email without @ returns failure" {
|
||||
validateEmail("userexample.com").shouldBeFailure()
|
||||
}
|
||||
})
|
||||
|
||||
// Adım 3: Testleri çalıştır - FAIL doğrula
|
||||
// $ ./gradlew test
|
||||
// EmailValidatorTest > valid email returns success FAILED
|
||||
// kotlin.NotImplementedError: An operation is not implemented
|
||||
|
||||
// Adım 4: Minimal kodu uygula (GREEN)
|
||||
fun validateEmail(email: String): Result<String> {
|
||||
if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
|
||||
if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
|
||||
val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
|
||||
if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
|
||||
return Result.success(email)
|
||||
}
|
||||
|
||||
// Adım 5: Testleri çalıştır - PASS doğrula
|
||||
// $ ./gradlew test
|
||||
// EmailValidatorTest > valid email returns success PASSED
|
||||
// EmailValidatorTest > empty email returns failure PASSED
|
||||
// EmailValidatorTest > email without @ returns failure PASSED
|
||||
|
||||
// Adım 6: Gerekirse refactor et, testlerin hala geçtiğini doğrula
|
||||
```
|
||||
|
||||
## Kotest Spec Stilleri
|
||||
|
||||
### StringSpec (En Basit)
|
||||
|
||||
```kotlin
|
||||
class CalculatorTest : StringSpec({
|
||||
"add two positive numbers" {
|
||||
Calculator.add(2, 3) shouldBe 5
|
||||
}
|
||||
|
||||
"add negative numbers" {
|
||||
Calculator.add(-1, -2) shouldBe -3
|
||||
}
|
||||
|
||||
"add zero" {
|
||||
Calculator.add(0, 5) shouldBe 5
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### FunSpec (JUnit benzeri)
|
||||
|
||||
```kotlin
|
||||
class UserServiceTest : FunSpec({
|
||||
val repository = mockk<UserRepository>()
|
||||
val service = UserService(repository)
|
||||
|
||||
test("getUser returns user when found") {
|
||||
val expected = User(id = "1", name = "Alice")
|
||||
coEvery { repository.findById("1") } returns expected
|
||||
|
||||
val result = service.getUser("1")
|
||||
|
||||
result shouldBe expected
|
||||
}
|
||||
|
||||
test("getUser throws when not found") {
|
||||
coEvery { repository.findById("999") } returns null
|
||||
|
||||
shouldThrow<UserNotFoundException> {
|
||||
service.getUser("999")
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### BehaviorSpec (BDD Stili)
|
||||
|
||||
```kotlin
|
||||
class OrderServiceTest : BehaviorSpec({
|
||||
val repository = mockk<OrderRepository>()
|
||||
val paymentService = mockk<PaymentService>()
|
||||
val service = OrderService(repository, paymentService)
|
||||
|
||||
Given("a valid order request") {
|
||||
val request = CreateOrderRequest(
|
||||
userId = "user-1",
|
||||
items = listOf(OrderItem("product-1", quantity = 2)),
|
||||
)
|
||||
|
||||
When("the order is placed") {
|
||||
coEvery { paymentService.charge(any()) } returns PaymentResult.Success
|
||||
coEvery { repository.save(any()) } answers { firstArg() }
|
||||
|
||||
val result = service.placeOrder(request)
|
||||
|
||||
Then("it should return a confirmed order") {
|
||||
result.status shouldBe OrderStatus.CONFIRMED
|
||||
}
|
||||
|
||||
Then("it should charge payment") {
|
||||
coVerify(exactly = 1) { paymentService.charge(any()) }
|
||||
}
|
||||
}
|
||||
|
||||
When("payment fails") {
|
||||
coEvery { paymentService.charge(any()) } returns PaymentResult.Declined
|
||||
|
||||
Then("it should throw PaymentException") {
|
||||
shouldThrow<PaymentException> {
|
||||
service.placeOrder(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Kotest Matcher'lar
|
||||
|
||||
### Temel Matcher'lar
|
||||
|
||||
```kotlin
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import io.kotest.matchers.string.*
|
||||
import io.kotest.matchers.collections.*
|
||||
import io.kotest.matchers.nulls.*
|
||||
|
||||
// Eşitlik
|
||||
result shouldBe expected
|
||||
result shouldNotBe unexpected
|
||||
|
||||
// String'ler
|
||||
name shouldStartWith "Al"
|
||||
name shouldEndWith "ice"
|
||||
name shouldContain "lic"
|
||||
name shouldMatch Regex("[A-Z][a-z]+")
|
||||
name.shouldBeBlank()
|
||||
|
||||
// Koleksiyonlar
|
||||
list shouldContain "item"
|
||||
list shouldHaveSize 3
|
||||
list.shouldBeSorted()
|
||||
list.shouldContainAll("a", "b", "c")
|
||||
list.shouldBeEmpty()
|
||||
|
||||
// Null'lar
|
||||
result.shouldNotBeNull()
|
||||
result.shouldBeNull()
|
||||
|
||||
// Tipler
|
||||
result.shouldBeInstanceOf<User>()
|
||||
|
||||
// Sayılar
|
||||
count shouldBeGreaterThan 0
|
||||
price shouldBeInRange 1.0..100.0
|
||||
|
||||
// Exception'lar
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
validateAge(-1)
|
||||
}.message shouldBe "Age must be positive"
|
||||
|
||||
shouldNotThrow<Exception> {
|
||||
validateAge(25)
|
||||
}
|
||||
```
|
||||
|
||||
## MockK
|
||||
|
||||
### Temel Mocking
|
||||
|
||||
```kotlin
|
||||
class UserServiceTest : FunSpec({
|
||||
val repository = mockk<UserRepository>()
|
||||
val logger = mockk<Logger>(relaxed = true) // Relaxed: varsayılanları döndürür
|
||||
val service = UserService(repository, logger)
|
||||
|
||||
beforeTest {
|
||||
clearMocks(repository, logger)
|
||||
}
|
||||
|
||||
test("findUser delegates to repository") {
|
||||
val expected = User(id = "1", name = "Alice")
|
||||
every { repository.findById("1") } returns expected
|
||||
|
||||
val result = service.findUser("1")
|
||||
|
||||
result shouldBe expected
|
||||
verify(exactly = 1) { repository.findById("1") }
|
||||
}
|
||||
|
||||
test("findUser returns null for unknown id") {
|
||||
every { repository.findById(any()) } returns null
|
||||
|
||||
val result = service.findUser("unknown")
|
||||
|
||||
result.shouldBeNull()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Coroutine Mocking
|
||||
|
||||
```kotlin
|
||||
class AsyncUserServiceTest : FunSpec({
|
||||
val repository = mockk<UserRepository>()
|
||||
val service = UserService(repository)
|
||||
|
||||
test("getUser suspending function") {
|
||||
coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
|
||||
|
||||
val result = service.getUser("1")
|
||||
|
||||
result.name shouldBe "Alice"
|
||||
coVerify { repository.findById("1") }
|
||||
}
|
||||
|
||||
test("getUser with delay") {
|
||||
coEvery { repository.findById("1") } coAnswers {
|
||||
delay(100) // Async çalışmayı simüle et
|
||||
User(id = "1", name = "Alice")
|
||||
}
|
||||
|
||||
val result = service.getUser("1")
|
||||
result.name shouldBe "Alice"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Coroutine Testi
|
||||
|
||||
### Suspend Fonksiyonlar İçin runTest
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
||||
class CoroutineServiceTest : FunSpec({
|
||||
test("concurrent fetches complete together") {
|
||||
runTest {
|
||||
val service = DataService(testScope = this)
|
||||
|
||||
val result = service.fetchAllData()
|
||||
|
||||
result.users.shouldNotBeEmpty()
|
||||
result.products.shouldNotBeEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
test("timeout after delay") {
|
||||
runTest {
|
||||
val service = SlowService()
|
||||
|
||||
shouldThrow<TimeoutCancellationException> {
|
||||
withTimeout(100) {
|
||||
service.slowOperation() // > 100ms sürer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Flow Testi
|
||||
|
||||
```kotlin
|
||||
import io.kotest.matchers.collections.shouldContainInOrder
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
||||
class FlowServiceTest : FunSpec({
|
||||
test("observeUsers emits updates") {
|
||||
runTest {
|
||||
val service = UserFlowService()
|
||||
|
||||
val emissions = service.observeUsers()
|
||||
.take(3)
|
||||
.toList()
|
||||
|
||||
emissions shouldHaveSize 3
|
||||
emissions.last().shouldNotBeEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
test("searchUsers debounces input") {
|
||||
runTest {
|
||||
val service = SearchService()
|
||||
val queries = MutableSharedFlow<String>()
|
||||
|
||||
val results = mutableListOf<List<User>>()
|
||||
val job = launch {
|
||||
service.searchUsers(queries).collect { results.add(it) }
|
||||
}
|
||||
|
||||
queries.emit("a")
|
||||
queries.emit("ab")
|
||||
queries.emit("abc") // Sadece bu aramayı tetiklemeli
|
||||
advanceTimeBy(500)
|
||||
|
||||
results shouldHaveSize 1
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Property-Based Testing
|
||||
|
||||
### Kotest Property Testing
|
||||
|
||||
```kotlin
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.property.Arb
|
||||
import io.kotest.property.arbitrary.*
|
||||
import io.kotest.property.forAll
|
||||
import io.kotest.property.checkAll
|
||||
|
||||
class PropertyTest : FunSpec({
|
||||
test("string reverse is involutory") {
|
||||
forAll<String> { s ->
|
||||
s.reversed().reversed() == s
|
||||
}
|
||||
}
|
||||
|
||||
test("list sort is idempotent") {
|
||||
forAll(Arb.list(Arb.int())) { list ->
|
||||
list.sorted() == list.sorted().sorted()
|
||||
}
|
||||
}
|
||||
|
||||
test("serialization roundtrip preserves data") {
|
||||
checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
|
||||
User(name = name, email = "$email@test.com")
|
||||
}) { user ->
|
||||
val json = Json.encodeToString(user)
|
||||
val decoded = Json.decodeFromString<User>(json)
|
||||
decoded shouldBe user
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Kover Coverage
|
||||
|
||||
### Gradle Yapılandırması
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
plugins {
|
||||
id("org.jetbrains.kotlinx.kover") version "0.9.7"
|
||||
}
|
||||
|
||||
kover {
|
||||
reports {
|
||||
total {
|
||||
html { onCheck = true }
|
||||
xml { onCheck = true }
|
||||
}
|
||||
filters {
|
||||
excludes {
|
||||
classes("*.generated.*", "*.config.*")
|
||||
}
|
||||
}
|
||||
verify {
|
||||
rule {
|
||||
minBound(80) // %80 coverage'ın altında build başarısız
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Coverage Komutları
|
||||
|
||||
```bash
|
||||
# Testleri coverage ile çalıştır
|
||||
./gradlew koverHtmlReport
|
||||
|
||||
# Coverage eşiklerini doğrula
|
||||
./gradlew koverVerify
|
||||
|
||||
# CI için XML raporu
|
||||
./gradlew koverXmlReport
|
||||
|
||||
# HTML raporunu görüntüle (OS'nize göre komutu kullanın)
|
||||
# macOS: open build/reports/kover/html/index.html
|
||||
# Linux: xdg-open build/reports/kover/html/index.html
|
||||
# Windows: start build/reports/kover/html/index.html
|
||||
```
|
||||
|
||||
### Coverage Hedefleri
|
||||
|
||||
| Kod Tipi | Hedef |
|
||||
|-----------|--------|
|
||||
| Kritik business mantığı | %100 |
|
||||
| Public API'ler | %90+ |
|
||||
| Genel kod | %80+ |
|
||||
| Generated / config kodu | Hariç tut |
|
||||
|
||||
## Ktor testApplication Testi
|
||||
|
||||
```kotlin
|
||||
class ApiRoutesTest : FunSpec({
|
||||
test("GET /users returns list") {
|
||||
testApplication {
|
||||
application {
|
||||
configureRouting()
|
||||
configureSerialization()
|
||||
}
|
||||
|
||||
val response = client.get("/users")
|
||||
|
||||
response.status shouldBe HttpStatusCode.OK
|
||||
val users = response.body<List<UserResponse>>()
|
||||
users.shouldNotBeEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
test("POST /users creates user") {
|
||||
testApplication {
|
||||
application {
|
||||
configureRouting()
|
||||
configureSerialization()
|
||||
}
|
||||
|
||||
val response = client.post("/users") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateUserRequest("Alice", "alice@example.com"))
|
||||
}
|
||||
|
||||
response.status shouldBe HttpStatusCode.Created
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Test Komutları
|
||||
|
||||
```bash
|
||||
# Tüm testleri çalıştır
|
||||
./gradlew test
|
||||
|
||||
# Belirli test class'ını çalıştır
|
||||
./gradlew test --tests "com.example.UserServiceTest"
|
||||
|
||||
# Belirli testi çalıştır
|
||||
./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found"
|
||||
|
||||
# Verbose çıktı ile çalıştır
|
||||
./gradlew test --info
|
||||
|
||||
# Coverage ile çalıştır
|
||||
./gradlew koverHtmlReport
|
||||
|
||||
# Detekt çalıştır (statik analiz)
|
||||
./gradlew detekt
|
||||
|
||||
# Ktlint çalıştır (formatlama kontrolü)
|
||||
./gradlew ktlintCheck
|
||||
|
||||
# Sürekli test
|
||||
./gradlew test --continuous
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
**YAPILMASI GEREKENLER:**
|
||||
- ÖNCE testleri yaz (TDD)
|
||||
- Proje genelinde Kotest'in spec stillerini tutarlı kullan
|
||||
- Suspend fonksiyonlar için MockK'nın `coEvery`/`coVerify`'ını kullan
|
||||
- Coroutine testi için `runTest` kullan
|
||||
- İmplementasyon değil davranışı test et
|
||||
- Pure fonksiyonlar için property-based testing kullan
|
||||
- Netlik için `data class` test fixture'ları kullan
|
||||
|
||||
**YAPILMAMASI GEREKENLER:**
|
||||
- Test framework'lerini karıştırma (Kotest seç ve ona sadık kal)
|
||||
- Data class'ları mock'lama (gerçek instance'lar kullan)
|
||||
- Coroutine testlerinde `Thread.sleep()` kullanma (`advanceTimeBy` kullan)
|
||||
- TDD'de RED fazını atlama
|
||||
- Private fonksiyonları doğrudan test etme
|
||||
- Kararsız testleri görmezden gelme
|
||||
|
||||
## CI/CD ile Entegrasyon
|
||||
|
||||
```yaml
|
||||
# GitHub Actions örneği
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: ./gradlew test koverXmlReport
|
||||
|
||||
- name: Verify coverage
|
||||
run: ./gradlew koverVerify
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: build/reports/kover/report.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
```
|
||||
|
||||
**Hatırla**: Testler dokümantasyondur. Kotlin kodunuzun nasıl kullanılması gerektiğini gösterirler. Testleri okunabilir yapmak için Kotest'in açıklayıcı matcher'larını ve bağımlılıkları temiz mock'lamak için MockK kullanın.
|
||||
415
docs/tr/skills/laravel-patterns/SKILL.md
Normal file
415
docs/tr/skills/laravel-patterns/SKILL.md
Normal file
@@ -0,0 +1,415 @@
|
||||
---
|
||||
name: laravel-patterns
|
||||
description: Laravel architecture patterns, routing/controllers, Eloquent ORM, service layers, queues, events, caching, and API resources for production apps.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Laravel Geliştirme Desenleri
|
||||
|
||||
Ölçeklenebilir, bakım yapılabilir uygulamalar için üretim seviyesi Laravel mimari desenleri.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Laravel web uygulamaları veya API'ler oluşturma
|
||||
- Controller'lar, servisler ve domain mantığını yapılandırma
|
||||
- Eloquent model'ler ve ilişkiler ile çalışma
|
||||
- Resource'lar ve sayfalama ile API tasarlama
|
||||
- Kuyruklar, event'ler, caching ve arka plan işleri ekleme
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
- Uygulamayı net sınırlar etrafında yapılandırın (controller'lar -> servisler/action'lar -> model'ler).
|
||||
- Routing'i öngörülebilir tutmak için açık binding'ler ve scoped binding'ler kullanın; erişim kontrolü için yetkilendirmeyi yine de uygulayın.
|
||||
- Domain mantığını tutarlı tutmak için typed model'leri, cast'leri ve scope'ları tercih edin.
|
||||
- IO-ağır işleri kuyruklarda tutun ve pahalı okumaları önbelleğe alın.
|
||||
- Config'i `config/*` içinde merkezileştirin ve ortamları açık tutun.
|
||||
|
||||
## Örnekler
|
||||
|
||||
### Proje Yapısı
|
||||
|
||||
Net katman sınırları (HTTP, servisler/action'lar, model'ler) ile geleneksel bir Laravel düzeni kullanın.
|
||||
|
||||
### Önerilen Düzen
|
||||
|
||||
```
|
||||
app/
|
||||
├── Actions/ # Tek amaçlı kullanım durumları
|
||||
├── Console/
|
||||
├── Events/
|
||||
├── Exceptions/
|
||||
├── Http/
|
||||
│ ├── Controllers/
|
||||
│ ├── Middleware/
|
||||
│ ├── Requests/ # Form request validation
|
||||
│ └── Resources/ # API resources
|
||||
├── Jobs/
|
||||
├── Models/
|
||||
├── Policies/
|
||||
├── Providers/
|
||||
├── Services/ # Domain servislerini koordine etme
|
||||
└── Support/
|
||||
config/
|
||||
database/
|
||||
├── factories/
|
||||
├── migrations/
|
||||
└── seeders/
|
||||
resources/
|
||||
├── views/
|
||||
└── lang/
|
||||
routes/
|
||||
├── api.php
|
||||
├── web.php
|
||||
└── console.php
|
||||
```
|
||||
|
||||
### Controllers -> Services -> Actions
|
||||
|
||||
Controller'ları ince tutun. Orkestrasyon'u servislere ve tek amaçlı mantığı action'lara koyun.
|
||||
|
||||
```php
|
||||
final class CreateOrderAction
|
||||
{
|
||||
public function __construct(private OrderRepository $orders) {}
|
||||
|
||||
public function handle(CreateOrderData $data): Order
|
||||
{
|
||||
return $this->orders->create($data);
|
||||
}
|
||||
}
|
||||
|
||||
final class OrdersController extends Controller
|
||||
{
|
||||
public function __construct(private CreateOrderAction $createOrder) {}
|
||||
|
||||
public function store(StoreOrderRequest $request): JsonResponse
|
||||
{
|
||||
$order = $this->createOrder->handle($request->toDto());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => OrderResource::make($order),
|
||||
'error' => null,
|
||||
'meta' => null,
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Routing ve Controllers
|
||||
|
||||
Netlik için route-model binding ve resource controller'ları tercih edin.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::apiResource('projects', ProjectController::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Route Model Binding (Scoped)
|
||||
|
||||
Çapraz kiracı erişimini önlemek için scoped binding'leri kullanın.
|
||||
|
||||
```php
|
||||
Route::scopeBindings()->group(function () {
|
||||
Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
|
||||
});
|
||||
```
|
||||
|
||||
### İç İçe Route'lar ve Binding İsimleri
|
||||
|
||||
- Çift iç içe geçmeyi önlemek için prefix'leri ve path'leri tutarlı tutun (örn. `conversation` vs `conversations`).
|
||||
- Bound model'e uyan tek bir parametre ismi kullanın (örn. `Conversation` için `{conversation}`).
|
||||
- İç içe geçirirken üst-alt ilişkilerini zorlamak için scoped binding'leri tercih edin.
|
||||
|
||||
```php
|
||||
use App\Http\Controllers\Api\ConversationController;
|
||||
use App\Http\Controllers\Api\MessageController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth:sanctum')->prefix('conversations')->group(function () {
|
||||
Route::post('/', [ConversationController::class, 'store'])->name('conversations.store');
|
||||
|
||||
Route::scopeBindings()->group(function () {
|
||||
Route::get('/{conversation}', [ConversationController::class, 'show'])
|
||||
->name('conversations.show');
|
||||
|
||||
Route::post('/{conversation}/messages', [MessageController::class, 'store'])
|
||||
->name('conversation-messages.store');
|
||||
|
||||
Route::get('/{conversation}/messages/{message}', [MessageController::class, 'show'])
|
||||
->name('conversation-messages.show');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Bir parametrenin farklı bir model sınıfına çözümlenmesini istiyorsanız, açık binding tanımlayın. Özel binding mantığı için `Route::bind()` kullanın veya model'de `resolveRouteBinding()` uygulayın.
|
||||
|
||||
```php
|
||||
use App\Models\AiConversation;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::model('conversation', AiConversation::class);
|
||||
```
|
||||
|
||||
### Service Container Binding'leri
|
||||
|
||||
Net bağımlılık bağlantısı için bir service provider'da interface'leri implementasyonlara bağlayın.
|
||||
|
||||
```php
|
||||
use App\Repositories\EloquentOrderRepository;
|
||||
use App\Repositories\OrderRepository;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
final class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Eloquent Model Desenleri
|
||||
|
||||
### Model Yapılandırması
|
||||
|
||||
```php
|
||||
final class Project extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name', 'owner_id', 'status'];
|
||||
|
||||
protected $casts = [
|
||||
'status' => ProjectStatus::class,
|
||||
'archived_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner_id');
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNull('archived_at');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Özel Cast'ler ve Value Object'ler
|
||||
|
||||
Sıkı tiplemeler için enum'lar veya value object'leri kullanın.
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
protected $casts = [
|
||||
'status' => ProjectStatus::class,
|
||||
];
|
||||
```
|
||||
|
||||
```php
|
||||
protected function budgetCents(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (int $value) => Money::fromCents($value),
|
||||
set: fn (Money $money) => $money->toCents(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### N+1'i Önlemek için Eager Loading
|
||||
|
||||
```php
|
||||
$orders = Order::query()
|
||||
->with(['customer', 'items.product'])
|
||||
->latest()
|
||||
->paginate(25);
|
||||
```
|
||||
|
||||
### Karmaşık Filtreler için Query Object'leri
|
||||
|
||||
```php
|
||||
final class ProjectQuery
|
||||
{
|
||||
public function __construct(private Builder $query) {}
|
||||
|
||||
public function ownedBy(int $userId): self
|
||||
{
|
||||
$query = clone $this->query;
|
||||
|
||||
return new self($query->where('owner_id', $userId));
|
||||
}
|
||||
|
||||
public function active(): self
|
||||
{
|
||||
$query = clone $this->query;
|
||||
|
||||
return new self($query->whereNull('archived_at'));
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Scope'lar ve Soft Delete'ler
|
||||
|
||||
Varsayılan filtreleme için global scope'ları ve geri kurtarılabilir kayıtlar için `SoftDeletes` kullanın.
|
||||
Katmanlı davranış istemediğiniz sürece, aynı filtre için global scope veya named scope kullanın, ikisini birden değil.
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
final class Project extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope('active', function (Builder $builder): void {
|
||||
$builder->whereNull('archived_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Yeniden Kullanılabilir Filtreler için Query Scope'ları
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
final class Project extends Model
|
||||
{
|
||||
public function scopeOwnedBy(Builder $query, int $userId): Builder
|
||||
{
|
||||
return $query->where('owner_id', $userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Servis, repository vb. içinde
|
||||
$projects = Project::ownedBy($user->id)->get();
|
||||
```
|
||||
|
||||
### Çok Adımlı Güncellemeler için Transaction'lar
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
DB::transaction(function (): void {
|
||||
$order->update(['status' => 'paid']);
|
||||
$order->items()->update(['paid_at' => now()]);
|
||||
});
|
||||
```
|
||||
|
||||
### Migration'lar
|
||||
|
||||
### İsimlendirme Kuralı
|
||||
|
||||
- Dosya isimleri zaman damgası kullanır: `YYYY_MM_DD_HHMMSS_create_users_table.php`
|
||||
- Migration'lar anonim sınıflar kullanır (isimlendirilmiş sınıf yok); dosya ismi amacı iletir
|
||||
- Tablo isimleri varsayılan olarak `snake_case` ve çoğuldur
|
||||
|
||||
### Örnek Migration
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('orders', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('status', 32)->index();
|
||||
$table->unsignedInteger('total_cents');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('orders');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Form Request'ler ve Validation
|
||||
|
||||
Validation'ı form request'lerde tutun ve input'ları DTO'lara dönüştürün.
|
||||
|
||||
```php
|
||||
use App\Models\Order;
|
||||
|
||||
final class StoreOrderRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('create', Order::class) ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'customer_id' => ['required', 'integer', 'exists:customers,id'],
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.sku' => ['required', 'string'],
|
||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
public function toDto(): CreateOrderData
|
||||
{
|
||||
return new CreateOrderData(
|
||||
customerId: (int) $this->validated('customer_id'),
|
||||
items: $this->validated('items'),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Resource'ları
|
||||
|
||||
Resource'lar ve sayfalama ile API yanıtlarını tutarlı tutun.
|
||||
|
||||
```php
|
||||
$projects = Project::query()->active()->paginate(25);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => ProjectResource::collection($projects->items()),
|
||||
'error' => null,
|
||||
'meta' => [
|
||||
'page' => $projects->currentPage(),
|
||||
'per_page' => $projects->perPage(),
|
||||
'total' => $projects->total(),
|
||||
],
|
||||
]);
|
||||
```
|
||||
|
||||
### Event'ler, Job'lar ve Kuyruklar
|
||||
|
||||
- Yan etkiler için domain event'leri yayınlayın (email'ler, analytics)
|
||||
- Yavaş işler için kuyruğa alınmış job'ları kullanın (raporlar, export'lar, webhook'lar)
|
||||
- Yeniden deneme ve backoff ile idempotent handler'ları tercih edin
|
||||
|
||||
### Caching
|
||||
|
||||
- Okuma-ağırlıklı endpoint'leri ve pahalı sorguları önbelleğe alın
|
||||
- Model event'lerinde (created/updated/deleted) önbellekleri geçersiz kılın
|
||||
- Kolay geçersiz kılma için ilgili verileri önbelleğe alırken tag'leri kullanın
|
||||
|
||||
### Yapılandırma ve Ortamlar
|
||||
|
||||
- Gizli bilgileri `.env`'de ve yapılandırmayı `config/*.php`'de tutun
|
||||
- Ortama özel yapılandırma geçersiz kılmaları kullanın ve production'da `config:cache` kullanın
|
||||
285
docs/tr/skills/laravel-security/SKILL.md
Normal file
285
docs/tr/skills/laravel-security/SKILL.md
Normal file
@@ -0,0 +1,285 @@
|
||||
---
|
||||
name: laravel-security
|
||||
description: Laravel security best practices for authn/authz, validation, CSRF, mass assignment, file uploads, secrets, rate limiting, and secure deployment.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Laravel Güvenlik En İyi Uygulamaları
|
||||
|
||||
Laravel uygulamalarını yaygın güvenlik açıklarına karşı korumak için kapsamlı güvenlik rehberi.
|
||||
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
- Kimlik doğrulama veya yetkilendirme ekleme
|
||||
- Kullanıcı girişi ve dosya yüklemelerini işleme
|
||||
- Yeni API endpoint'leri oluşturma
|
||||
- Gizli bilgileri ve ortam ayarlarını yönetme
|
||||
- Production deployment'ları sertleştirme
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
- Middleware temel korumalar sağlar (CSRF için `VerifyCsrfToken`, güvenlik başlıkları için `SecurityHeaders`).
|
||||
- Guard'lar ve policy'ler erişim kontrolünü zorlar (`auth:sanctum`, `$this->authorize`, policy middleware).
|
||||
- Form Request'ler servislere ulaşmadan önce girişi doğrular ve şekillendirir (`UploadInvoiceRequest`).
|
||||
- Rate limiting, auth kontrolleri ile birlikte kötüye kullanım koruması ekler (`RateLimiter::for('login')`).
|
||||
- Veri güvenliği encrypted cast'lerden, mass-assignment korumalarından ve signed route'lardan gelir (`URL::temporarySignedRoute` + `signed` middleware).
|
||||
|
||||
## Temel Güvenlik Ayarları
|
||||
|
||||
- Production'da `APP_DEBUG=false`
|
||||
- `APP_KEY` ayarlanmalı ve tehlikeye girdiğinde döndürülmelidir
|
||||
- `SESSION_SECURE_COOKIE=true` ve `SESSION_SAME_SITE=lax` ayarlayın (veya hassas uygulamalar için `strict`)
|
||||
- Doğru HTTPS algılama için güvenilir proxy'leri yapılandırın
|
||||
|
||||
## Session ve Cookie Sertleştirme
|
||||
|
||||
- JavaScript erişimini önlemek için `SESSION_HTTP_ONLY=true` ayarlayın
|
||||
- Yüksek riskli akışlar için `SESSION_SAME_SITE=strict` kullanın
|
||||
- Login ve ayrıcalık değişikliklerinde session'ları yeniden oluşturun
|
||||
|
||||
## Kimlik Doğrulama ve Token'lar
|
||||
|
||||
- API kimlik doğrulama için Laravel Sanctum veya Passport kullanın
|
||||
- Hassas veriler için yenileme akışları ile kısa ömürlü token'ları tercih edin
|
||||
- Logout ve tehlikeye girmiş hesaplarda token'ları iptal edin
|
||||
|
||||
Örnek route koruması:
|
||||
|
||||
```php
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth:sanctum')->get('/me', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
```
|
||||
|
||||
## Parola Güvenliği
|
||||
|
||||
- `Hash::make()` ile parolaları hash'leyin ve asla düz metin saklamayın
|
||||
- Sıfırlama akışları için Laravel'in password broker'ını kullanın
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
$validated = $request->validate([
|
||||
'password' => ['required', 'string', Password::min(12)->letters()->mixedCase()->numbers()->symbols()],
|
||||
]);
|
||||
|
||||
$user->update(['password' => Hash::make($validated['password'])]);
|
||||
```
|
||||
|
||||
## Yetkilendirme: Policy'ler ve Gate'ler
|
||||
|
||||
- Model seviyesi yetkilendirme için policy'leri kullanın
|
||||
- Controller'larda ve servislerde yetkilendirmeyi zorlayın
|
||||
|
||||
```php
|
||||
$this->authorize('update', $project);
|
||||
```
|
||||
|
||||
Route seviyesi zorlama için policy middleware kullanın:
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::put('/projects/{project}', [ProjectController::class, 'update'])
|
||||
->middleware(['auth:sanctum', 'can:update,project']);
|
||||
```
|
||||
|
||||
## Validation ve Veri Temizleme
|
||||
|
||||
- Her zaman Form Request'ler ile girişleri doğrulayın
|
||||
- Sıkı validation kuralları ve tip kontrolleri kullanın
|
||||
- Türetilmiş alanlar için request payload'larına asla güvenmeyin
|
||||
|
||||
## Mass Assignment Koruması
|
||||
|
||||
- `$fillable` veya `$guarded` kullanın ve `Model::unguard()` kullanmaktan kaçının
|
||||
- DTO'ları veya açık attribute mapping'i tercih edin
|
||||
|
||||
## SQL Injection Önleme
|
||||
|
||||
- Eloquent veya query builder parametre binding kullanın
|
||||
- Kesinlikle gerekli olmadıkça raw SQL kullanmaktan kaçının
|
||||
|
||||
```php
|
||||
DB::select('select * from users where email = ?', [$email]);
|
||||
```
|
||||
|
||||
## XSS Önleme
|
||||
|
||||
- Blade varsayılan olarak çıktıyı escape eder (`{{ }}`)
|
||||
- `{!! !!}` sadece güvenilir, temizlenmiş HTML için kullanın
|
||||
- Zengin metni özel bir kütüphane ile temizleyin
|
||||
|
||||
## CSRF Koruması
|
||||
|
||||
- `VerifyCsrfToken` middleware'ini etkin tutun
|
||||
- Formlara `@csrf` ekleyin ve SPA istekleri için XSRF token'ları gönderin
|
||||
|
||||
Sanctum ile SPA kimlik doğrulaması için, stateful isteklerin yapılandırıldığından emin olun:
|
||||
|
||||
```php
|
||||
// config/sanctum.php
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')),
|
||||
```
|
||||
|
||||
## Dosya Yükleme Güvenliği
|
||||
|
||||
- Dosya boyutunu, MIME tipini ve uzantısını doğrulayın
|
||||
- Mümkün olduğunda yüklemeleri public path dışında saklayın
|
||||
- Gerekirse dosyaları malware için tarayın
|
||||
|
||||
```php
|
||||
final class UploadInvoiceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return (bool) $this->user()?->can('upload-invoice');
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'invoice' => ['required', 'file', 'mimes:pdf', 'max:5120'],
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
$path = $request->file('invoice')->store(
|
||||
'invoices',
|
||||
config('filesystems.private_disk', 'local') // bunu public olmayan bir disk'e ayarlayın
|
||||
);
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Auth ve yazma endpoint'lerinde `throttle` middleware'i uygulayın
|
||||
- Login, password reset ve OTP için daha sıkı limitler kullanın
|
||||
|
||||
```php
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
return [
|
||||
Limit::perMinute(5)->by($request->ip()),
|
||||
Limit::perMinute(5)->by(strtolower((string) $request->input('email'))),
|
||||
];
|
||||
});
|
||||
```
|
||||
|
||||
## Gizli Bilgiler ve Kimlik Bilgileri
|
||||
|
||||
- Gizli bilgileri asla kaynak kontrolüne commit etmeyin
|
||||
- Ortam değişkenlerini ve gizli yöneticileri kullanın
|
||||
- Maruz kalma sonrası anahtarları döndürün ve session'ları geçersiz kılın
|
||||
|
||||
## Şifreli Attribute'lar
|
||||
|
||||
Bekleyen hassas sütunlar için encrypted cast'leri kullanın.
|
||||
|
||||
```php
|
||||
protected $casts = [
|
||||
'api_token' => 'encrypted',
|
||||
];
|
||||
```
|
||||
|
||||
## Güvenlik Başlıkları
|
||||
|
||||
- Uygun yerlerde CSP, HSTS ve frame koruması ekleyin
|
||||
- HTTPS yönlendirmelerini zorlamak için güvenilir proxy yapılandırması kullanın
|
||||
|
||||
Başlıkları ayarlamak için örnek middleware:
|
||||
|
||||
```php
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class SecurityHeaders
|
||||
{
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$response->headers->add([
|
||||
'Content-Security-Policy' => "default-src 'self'",
|
||||
'Strict-Transport-Security' => 'max-age=31536000', // tüm subdomain'ler HTTPS olduğunda includeSubDomains/preload ekleyin
|
||||
'X-Frame-Options' => 'DENY',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'Referrer-Policy' => 'no-referrer',
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CORS ve API Erişimi
|
||||
|
||||
- `config/cors.php`'de origin'leri kısıtlayın
|
||||
- Kimlik doğrulamalı route'lar için wildcard origin'lerden kaçının
|
||||
|
||||
```php
|
||||
// config/cors.php
|
||||
return [
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
'allowed_origins' => ['https://app.example.com'],
|
||||
'allowed_headers' => [
|
||||
'Content-Type',
|
||||
'Authorization',
|
||||
'X-Requested-With',
|
||||
'X-XSRF-TOKEN',
|
||||
'X-CSRF-TOKEN',
|
||||
],
|
||||
'supports_credentials' => true,
|
||||
];
|
||||
```
|
||||
|
||||
## Loglama ve PII
|
||||
|
||||
- Parolaları, token'ları veya tam kart verilerini asla loglamayın
|
||||
- Yapılandırılmış loglarda hassas alanları redakte edin
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
Log::info('User updated profile', [
|
||||
'user_id' => $user->id,
|
||||
'email' => '[REDACTED]',
|
||||
'token' => '[REDACTED]',
|
||||
]);
|
||||
```
|
||||
|
||||
## Bağımlılık Güvenliği
|
||||
|
||||
- Düzenli olarak `composer audit` çalıştırın
|
||||
- Bağımlılıkları dikkatle sabitleyin ve CVE'lerde hızlıca güncelleyin
|
||||
|
||||
## Signed URL'ler
|
||||
|
||||
Geçici, kurcalamaya dayanıklı bağlantılar için signed route'ları kullanın.
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
$url = URL::temporarySignedRoute(
|
||||
'downloads.invoice',
|
||||
now()->addMinutes(15),
|
||||
['invoice' => $invoice->id]
|
||||
);
|
||||
```
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/invoices/{invoice}/download', [InvoiceController::class, 'download'])
|
||||
->name('downloads.invoice')
|
||||
->middleware('signed');
|
||||
```
|
||||
283
docs/tr/skills/laravel-tdd/SKILL.md
Normal file
283
docs/tr/skills/laravel-tdd/SKILL.md
Normal file
@@ -0,0 +1,283 @@
|
||||
---
|
||||
name: laravel-tdd
|
||||
description: Test-driven development for Laravel with PHPUnit and Pest, factories, database testing, fakes, and coverage targets.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Laravel TDD İş Akışı
|
||||
|
||||
80%+ kapsam (unit + feature) ile Laravel uygulamaları için test-driven development.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Laravel'de yeni özellikler veya endpoint'ler
|
||||
- Bug düzeltmeleri veya refactoring'ler
|
||||
- Eloquent model'leri, policy'leri, job'ları ve notification'ları test etme
|
||||
- Proje zaten PHPUnit'te standartlaşmamışsa yeni testler için Pest'i tercih edin
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
### Red-Green-Refactor Döngüsü
|
||||
|
||||
1) Başarısız bir test yazın
|
||||
2) Geçmek için minimal değişiklik uygulayın
|
||||
3) Testleri yeşil tutarken refactor edin
|
||||
|
||||
### Test Katmanları
|
||||
|
||||
- **Unit**: saf PHP sınıfları, value object'leri, servisler
|
||||
- **Feature**: HTTP endpoint'leri, auth, validation, policy'ler
|
||||
- **Integration**: database + kuyruk + harici sınırlar
|
||||
|
||||
Kapsama göre katmanları seçin:
|
||||
|
||||
- Saf iş mantığı ve servisler için **Unit** testleri kullanın.
|
||||
- HTTP, auth, validation ve yanıt şekli için **Feature** testleri kullanın.
|
||||
- DB/kuyruklar/harici servisleri birlikte doğrularken **Integration** testleri kullanın.
|
||||
|
||||
### Database Stratejisi
|
||||
|
||||
- Çoğu feature/integration testi için `RefreshDatabase` (test run'ı başına bir kez migration'ları çalıştırır, ardından desteklendiğinde her testi bir transaction'a sarar; in-memory veritabanları test başına yeniden migrate edebilir)
|
||||
- Şema zaten migrate edilmişse ve sadece test başına rollback'e ihtiyacınız varsa `DatabaseTransactions`
|
||||
- Her test için tam bir migrate/fresh'e ihtiyacınız varsa ve maliyetini karşılayabiliyorsanız `DatabaseMigrations`
|
||||
|
||||
Veritabanına dokunan testler için varsayılan olarak `RefreshDatabase` kullanın: transaction desteği olan veritabanları için, test run'ı başına bir kez (static bir bayrak aracılığıyla) migration'ları çalıştırır ve her testi bir transaction'a sarar; `:memory:` SQLite veya transaction'sız bağlantılar için her testten önce migrate eder. Şema zaten migrate edilmişse ve sadece test başına rollback'lere ihtiyacınız varsa `DatabaseTransactions` kullanın.
|
||||
|
||||
### Test Framework Seçimi
|
||||
|
||||
- Mevcut olduğunda yeni testler için varsayılan olarak **Pest** kullanın.
|
||||
- Proje zaten PHPUnit'te standartlaşmışsa veya PHPUnit'e özgü araçlar gerektiriyorsa sadece **PHPUnit** kullanın.
|
||||
|
||||
## Örnekler
|
||||
|
||||
### PHPUnit Örneği
|
||||
|
||||
```php
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ProjectControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_owner_can_create_project(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/api/projects', [
|
||||
'name' => 'New Project',
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$this->assertDatabaseHas('projects', ['name' => 'New Project']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Feature Test Örneği (HTTP Katmanı)
|
||||
|
||||
```php
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ProjectIndexTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_projects_index_returns_paginated_results(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
Project::factory()->count(3)->for($user)->create();
|
||||
|
||||
$response = $this->actingAs($user)->getJson('/api/projects');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pest Örneği
|
||||
|
||||
```php
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
use function Pest\Laravel\assertDatabaseHas;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('owner can create project', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = actingAs($user)->postJson('/api/projects', [
|
||||
'name' => 'New Project',
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
assertDatabaseHas('projects', ['name' => 'New Project']);
|
||||
});
|
||||
```
|
||||
|
||||
### Feature Test Pest Örneği (HTTP Katmanı)
|
||||
|
||||
```php
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('projects index returns paginated results', function () {
|
||||
$user = User::factory()->create();
|
||||
Project::factory()->count(3)->for($user)->create();
|
||||
|
||||
$response = actingAs($user)->getJson('/api/projects');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
|
||||
});
|
||||
```
|
||||
|
||||
### Factory'ler ve State'ler
|
||||
|
||||
- Test verileri için factory'leri kullanın
|
||||
- Uç durumlar için state'leri tanımlayın (archived, admin, trial)
|
||||
|
||||
```php
|
||||
$user = User::factory()->state(['role' => 'admin'])->create();
|
||||
```
|
||||
|
||||
### Database Testi
|
||||
|
||||
- Temiz durum için `RefreshDatabase` kullanın
|
||||
- Testleri izole ve deterministik tutun
|
||||
- Manuel sorgular yerine `assertDatabaseHas` tercih edin
|
||||
|
||||
### Persistence Test Örneği
|
||||
|
||||
```php
|
||||
use App\Models\Project;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ProjectRepositoryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_project_can_be_retrieved_by_slug(): void
|
||||
{
|
||||
$project = Project::factory()->create(['slug' => 'alpha']);
|
||||
|
||||
$found = Project::query()->where('slug', 'alpha')->firstOrFail();
|
||||
|
||||
$this->assertSame($project->id, $found->id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Yan Etkiler için Fake'ler
|
||||
|
||||
- Job'lar için `Bus::fake()`
|
||||
- Kuyruğa alınmış işler için `Queue::fake()`
|
||||
- Bildirimler için `Mail::fake()` ve `Notification::fake()`
|
||||
- Domain event'leri için `Event::fake()`
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
Queue::fake();
|
||||
|
||||
dispatch(new SendOrderConfirmation($order->id));
|
||||
|
||||
Queue::assertPushed(SendOrderConfirmation::class);
|
||||
```
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$user->notify(new InvoiceReady($invoice));
|
||||
|
||||
Notification::assertSentTo($user, InvoiceReady::class);
|
||||
```
|
||||
|
||||
### Auth Testi (Sanctum)
|
||||
|
||||
```php
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/projects');
|
||||
$response->assertOk();
|
||||
```
|
||||
|
||||
### HTTP ve Harici Servisler
|
||||
|
||||
- Harici API'leri izole etmek için `Http::fake()` kullanın
|
||||
- Giden payload'ları `Http::assertSent()` ile doğrulayın
|
||||
|
||||
### Kapsam Hedefleri
|
||||
|
||||
- Unit + feature testleri için 80%+ kapsam zorlayın
|
||||
- CI'da `pcov` veya `XDEBUG_MODE=coverage` kullanın
|
||||
|
||||
### Test Komutları
|
||||
|
||||
- `php artisan test`
|
||||
- `vendor/bin/phpunit`
|
||||
- `vendor/bin/pest`
|
||||
|
||||
### Test Yapılandırması
|
||||
|
||||
- Hızlı testler için `phpunit.xml`'de `DB_CONNECTION=sqlite` ve `DB_DATABASE=:memory:` ayarlayın
|
||||
- Dev/prod verilerine dokunmaktan kaçınmak için testler için ayrı env tutun
|
||||
|
||||
### Yetkilendirme Testleri
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
$this->assertTrue(Gate::forUser($user)->allows('update', $project));
|
||||
$this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
|
||||
```
|
||||
|
||||
### Inertia Feature Testleri
|
||||
|
||||
Inertia.js kullanırken, Inertia test yardımcıları ile component ismi ve prop'ları doğrulayın.
|
||||
|
||||
```php
|
||||
use App\Models\User;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class DashboardInertiaTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_dashboard_inertia_props(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/dashboard');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Dashboard')
|
||||
->where('user.id', $user->id)
|
||||
->has('projects')
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Testleri Inertia yanıtlarıyla uyumlu tutmak için ham JSON assertion'ları yerine `assertInertia` tercih edin.
|
||||
179
docs/tr/skills/laravel-verification/SKILL.md
Normal file
179
docs/tr/skills/laravel-verification/SKILL.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
name: laravel-verification
|
||||
description: Verification loop for Laravel projects: env checks, linting, static analysis, tests with coverage, security scans, and deployment readiness.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Laravel Doğrulama Döngüsü
|
||||
|
||||
PR'lardan önce, büyük değişikliklerden sonra ve deployment öncesi çalıştırın.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Laravel projesi için pull request açmadan önce
|
||||
- Büyük refactoring'ler veya bağımlılık yükseltmelerinden sonra
|
||||
- Staging veya production için deployment öncesi doğrulama
|
||||
- Tam lint -> test -> güvenlik -> deployment hazırlık pipeline'ı çalıştırma
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
- Her katmanın bir öncekinin üzerine inşa edilmesi için fazları sırayla ortam kontrollerinden deployment hazırlığına kadar çalıştırın.
|
||||
- Ortam ve Composer kontrolleri her şeyi kapsar; başarısız olurlarsa hemen durun.
|
||||
- Tam testleri ve kapsamı çalıştırmadan önce linting/static analiz temiz olmalıdır.
|
||||
- Güvenlik ve migration incelemeleri testlerden sonra olur, böylece veri veya yayın adımlarından önce davranışı doğrularsınız.
|
||||
- Build/deployment hazırlığı ve kuyruk/zamanlayıcı kontrolleri son kapılardır; herhangi bir başarısızlık yayını engeller.
|
||||
|
||||
## Faz 1: Ortam Kontrolleri
|
||||
|
||||
```bash
|
||||
php -v
|
||||
composer --version
|
||||
php artisan --version
|
||||
```
|
||||
|
||||
- `.env`'nin mevcut olduğunu ve gerekli anahtarların var olduğunu doğrulayın
|
||||
- Production ortamları için `APP_DEBUG=false` onaylayın
|
||||
- `APP_ENV`'in hedef deployment'la eşleştiğini onaylayın (`production`, `staging`)
|
||||
|
||||
Yerel olarak Laravel Sail kullanıyorsanız:
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail php -v
|
||||
./vendor/bin/sail artisan --version
|
||||
```
|
||||
|
||||
## Faz 1.5: Composer ve Autoload
|
||||
|
||||
```bash
|
||||
composer validate
|
||||
composer dump-autoload -o
|
||||
```
|
||||
|
||||
## Faz 2: Linting ve Static Analiz
|
||||
|
||||
```bash
|
||||
vendor/bin/pint --test
|
||||
vendor/bin/phpstan analyse
|
||||
```
|
||||
|
||||
Projeniz PHPStan yerine Psalm kullanıyorsa:
|
||||
|
||||
```bash
|
||||
vendor/bin/psalm
|
||||
```
|
||||
|
||||
## Faz 3: Testler ve Kapsam
|
||||
|
||||
```bash
|
||||
php artisan test
|
||||
```
|
||||
|
||||
Kapsam (CI):
|
||||
|
||||
```bash
|
||||
XDEBUG_MODE=coverage php artisan test --coverage
|
||||
```
|
||||
|
||||
CI örneği (format -> static analiz -> testler):
|
||||
|
||||
```bash
|
||||
vendor/bin/pint --test
|
||||
vendor/bin/phpstan analyse
|
||||
XDEBUG_MODE=coverage php artisan test --coverage
|
||||
```
|
||||
|
||||
## Faz 4: Güvenlik ve Bağımlılık Kontrolleri
|
||||
|
||||
```bash
|
||||
composer audit
|
||||
```
|
||||
|
||||
## Faz 5: Database ve Migration'lar
|
||||
|
||||
```bash
|
||||
php artisan migrate --pretend
|
||||
php artisan migrate:status
|
||||
```
|
||||
|
||||
- Yıkıcı migration'ları dikkatle inceleyin
|
||||
- Migration dosya isimlerinin `Y_m_d_His_*` formatını takip ettiğinden emin olun (örn. `2025_03_14_154210_create_orders_table.php`) ve değişikliği net bir şekilde açıklasın
|
||||
- Rollback'lerin mümkün olduğundan emin olun
|
||||
- `down()` metotlarını doğrulayın ve açık yedeklemeler olmadan geri alınamaz veri kaybından kaçının
|
||||
|
||||
## Faz 6: Build ve Deployment Hazırlığı
|
||||
|
||||
```bash
|
||||
php artisan optimize:clear
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
```
|
||||
|
||||
- Cache warmup'larının production yapılandırmasında başarılı olduğundan emin olun
|
||||
- Kuyruk worker'larının ve zamanlayıcının yapılandırıldığını doğrulayın
|
||||
- Hedef ortamda `storage/` ve `bootstrap/cache/`'in yazılabilir olduğunu onaylayın
|
||||
|
||||
## Faz 7: Kuyruk ve Zamanlayıcı Kontrolleri
|
||||
|
||||
```bash
|
||||
php artisan schedule:list
|
||||
php artisan queue:failed
|
||||
```
|
||||
|
||||
Horizon kullanılıyorsa:
|
||||
|
||||
```bash
|
||||
php artisan horizon:status
|
||||
```
|
||||
|
||||
`queue:monitor` mevcutsa, job'ları işlemeden biriktirmeyi kontrol etmek için kullanın:
|
||||
|
||||
```bash
|
||||
php artisan queue:monitor default --max=100
|
||||
```
|
||||
|
||||
Aktif doğrulama (sadece staging): özel bir kuyruğa no-op job dispatch edin ve işlemek için tek bir worker çalıştırın (non-`sync` kuyruk bağlantısının yapılandırıldığından emin olun).
|
||||
|
||||
```bash
|
||||
php artisan tinker --execute="dispatch((new App\\Jobs\\QueueHealthcheck())->onQueue('healthcheck'))"
|
||||
php artisan queue:work --once --queue=healthcheck
|
||||
```
|
||||
|
||||
Job'un beklenen yan etkiyi ürettiğini doğrulayın (log girişi, healthcheck tablo satırı veya metrik).
|
||||
|
||||
Bunu sadece test job'u işlemenin güvenli olduğu non-production ortamlarında çalıştırın.
|
||||
|
||||
## Örnekler
|
||||
|
||||
Minimal akış:
|
||||
|
||||
```bash
|
||||
php -v
|
||||
composer --version
|
||||
php artisan --version
|
||||
composer validate
|
||||
vendor/bin/pint --test
|
||||
vendor/bin/phpstan analyse
|
||||
php artisan test
|
||||
composer audit
|
||||
php artisan migrate --pretend
|
||||
php artisan config:cache
|
||||
php artisan queue:failed
|
||||
```
|
||||
|
||||
CI tarzı pipeline:
|
||||
|
||||
```bash
|
||||
composer validate
|
||||
composer dump-autoload -o
|
||||
vendor/bin/pint --test
|
||||
vendor/bin/phpstan analyse
|
||||
XDEBUG_MODE=coverage php artisan test --coverage
|
||||
composer audit
|
||||
php artisan migrate --pretend
|
||||
php artisan optimize:clear
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan schedule:list
|
||||
```
|
||||
44
docs/tr/skills/nextjs-turbopack/SKILL.md
Normal file
44
docs/tr/skills/nextjs-turbopack/SKILL.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: nextjs-turbopack
|
||||
description: Next.js 16+ and Turbopack — incremental bundling, FS caching, dev speed, and when to use Turbopack vs webpack.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Next.js ve Turbopack
|
||||
|
||||
Next.js 16+ yerel geliştirme için varsayılan olarak Turbopack kullanır: geliştirme başlatma ve hot update'leri önemli ölçüde hızlandıran Rust ile yazılmış artımlı bir bundler.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- **Turbopack (varsayılan dev)**: Günlük geliştirme için kullanın. Özellikle büyük uygulamalarda daha hızlı soğuk başlatma ve HMR.
|
||||
- **Webpack (legacy dev)**: Sadece bir Turbopack bug'ına denk gelirseniz veya dev'de webpack'e özgü bir plugin'e güveniyorsanız kullanın. `--webpack` ile devre dışı bırakın (veya Next.js sürümünüze bağlı olarak `--no-turbopack`; sürümünüz için dokümanlara bakın).
|
||||
- **Production**: Production build davranışı (`next build`) Next.js sürümüne bağlı olarak Turbopack veya webpack kullanabilir; sürümünüz için resmi Next.js dokümantasyonunu kontrol edin.
|
||||
|
||||
Şu durumlarda kullanın: Next.js 16+ uygulamalarını geliştirme veya debug etme, yavaş dev başlatma veya HMR'yi teşhis etme veya production bundle'larını optimize etme.
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
- **Turbopack**: Next.js dev için artımlı bundler. Dosya sistemi önbelleği kullanır, böylece yeniden başlatmalar çok daha hızlıdır (örn. büyük projelerde 5-14x).
|
||||
- **Dev'de varsayılan**: Next.js 16'dan itibaren, `next dev` devre dışı bırakılmadıkça Turbopack ile çalışır.
|
||||
- **Dosya sistemi önbelleği**: Yeniden başlatmalar önceki çalışmayı yeniden kullanır; önbellek genellikle `.next` altındadır; temel kullanım için ekstra yapılandırma gerekmez.
|
||||
- **Bundle Analyzer (Next.js 16.1+)**: Çıktıyı incelemek ve ağır bağımlılıkları bulmak için deneysel Bundle Analyzer; config veya deneysel bayrak ile etkinleştirin (sürümünüz için Next.js dokümantasyonuna bakın).
|
||||
|
||||
## Örnekler
|
||||
|
||||
### Komutlar
|
||||
|
||||
```bash
|
||||
next dev
|
||||
next build
|
||||
next start
|
||||
```
|
||||
|
||||
### Kullanım
|
||||
|
||||
Turbopack ile yerel geliştirme için `next dev` çalıştırın. Code-splitting'i optimize etmek ve büyük bağımlılıkları kırpmak için Bundle Analyzer'ı kullanın (Next.js dokümantasyonuna bakın). Mümkün olduğunda App Router ve server component'leri tercih edin.
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
- Kararlı Turbopack ve önbellekleme davranışı için güncel bir Next.js 16.x sürümünde kalın.
|
||||
- Dev yavaşsa, Turbopack'te (varsayılan) olduğunuzdan ve önbelleğin gereksiz yere temizlenmediğinden emin olun.
|
||||
- Production bundle boyutu sorunları için, sürümünüz için resmi Next.js bundle analiz araçlarını kullanın.
|
||||
147
docs/tr/skills/postgres-patterns/SKILL.md
Normal file
147
docs/tr/skills/postgres-patterns/SKILL.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: postgres-patterns
|
||||
description: Sorgu optimizasyonu, şema tasarımı, indeksleme ve güvenlik için PostgreSQL veritabanı kalıpları. Supabase en iyi uygulamalarına dayanır.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# PostgreSQL Kalıpları
|
||||
|
||||
PostgreSQL en iyi uygulamaları için hızlı referans. Detaylı kılavuz için `database-reviewer` agent'ını kullanın.
|
||||
|
||||
## Ne Zaman Aktifleştirmeli
|
||||
|
||||
- SQL sorguları veya migration'lar yazarken
|
||||
- Veritabanı şemaları tasarlarken
|
||||
- Yavaş sorguları troubleshoot ederken
|
||||
- Row Level Security uygularken
|
||||
- Connection pooling kurarken
|
||||
|
||||
## Hızlı Referans
|
||||
|
||||
### İndeks Hile Sayfası
|
||||
|
||||
| Sorgu Kalıbı | İndeks Tipi | Örnek |
|
||||
|--------------|------------|---------|
|
||||
| `WHERE col = value` | B-tree (varsayılan) | `CREATE INDEX idx ON t (col)` |
|
||||
| `WHERE col > value` | B-tree | `CREATE INDEX idx ON t (col)` |
|
||||
| `WHERE a = x AND b > y` | Composite | `CREATE INDEX idx ON t (a, b)` |
|
||||
| `WHERE jsonb @> '{}'` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
||||
| `WHERE tsv @@ query` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
||||
| Zaman serisi aralıkları | BRIN | `CREATE INDEX idx ON t USING brin (col)` |
|
||||
|
||||
### Veri Tipi Hızlı Referans
|
||||
|
||||
| Kullanım Senaryosu | Doğru Tip | Kaçın |
|
||||
|----------|-------------|-------|
|
||||
| ID'ler | `bigint` | `int`, rastgele UUID |
|
||||
| String'ler | `text` | `varchar(255)` |
|
||||
| Timestamp'ler | `timestamptz` | `timestamp` |
|
||||
| Para | `numeric(10,2)` | `float` |
|
||||
| Flag'ler | `boolean` | `varchar`, `int` |
|
||||
|
||||
### Yaygın Kalıplar
|
||||
|
||||
**Composite İndeks Sırası:**
|
||||
```sql
|
||||
-- Önce eşitlik sütunları, sonra aralık sütunları
|
||||
CREATE INDEX idx ON orders (status, created_at);
|
||||
-- Şunlar için çalışır: WHERE status = 'pending' AND created_at > '2024-01-01'
|
||||
```
|
||||
|
||||
**Covering İndeks:**
|
||||
```sql
|
||||
CREATE INDEX idx ON users (email) INCLUDE (name, created_at);
|
||||
-- SELECT email, name, created_at için tablo aramasını önler
|
||||
```
|
||||
|
||||
**Partial İndeks:**
|
||||
```sql
|
||||
CREATE INDEX idx ON users (email) WHERE deleted_at IS NULL;
|
||||
-- Daha küçük indeks, sadece aktif kullanıcıları içerir
|
||||
```
|
||||
|
||||
**RLS Policy (Optimize Edilmiş):**
|
||||
```sql
|
||||
CREATE POLICY policy ON orders
|
||||
USING ((SELECT auth.uid()) = user_id); -- SELECT'e sar!
|
||||
```
|
||||
|
||||
**UPSERT:**
|
||||
```sql
|
||||
INSERT INTO settings (user_id, key, value)
|
||||
VALUES (123, 'theme', 'dark')
|
||||
ON CONFLICT (user_id, key)
|
||||
DO UPDATE SET value = EXCLUDED.value;
|
||||
```
|
||||
|
||||
**Cursor Sayfalama:**
|
||||
```sql
|
||||
SELECT * FROM products WHERE id > $last_id ORDER BY id LIMIT 20;
|
||||
-- O(1) vs O(n) olan OFFSET
|
||||
```
|
||||
|
||||
**Kuyruk İşleme:**
|
||||
```sql
|
||||
UPDATE jobs SET status = 'processing'
|
||||
WHERE id = (
|
||||
SELECT id FROM jobs WHERE status = 'pending'
|
||||
ORDER BY created_at LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
) RETURNING *;
|
||||
```
|
||||
|
||||
### Anti-Kalıp Tespiti
|
||||
|
||||
```sql
|
||||
-- İndekslenmemiş foreign key'leri bul
|
||||
SELECT conrelid::regclass, a.attname
|
||||
FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
||||
WHERE c.contype = 'f'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_index i
|
||||
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
|
||||
);
|
||||
|
||||
-- Yavaş sorguları bul
|
||||
SELECT query, mean_exec_time, calls
|
||||
FROM pg_stat_statements
|
||||
WHERE mean_exec_time > 100
|
||||
ORDER BY mean_exec_time DESC;
|
||||
|
||||
-- Tablo bloat'ını kontrol et
|
||||
SELECT relname, n_dead_tup, last_vacuum
|
||||
FROM pg_stat_user_tables
|
||||
WHERE n_dead_tup > 1000
|
||||
ORDER BY n_dead_tup DESC;
|
||||
```
|
||||
|
||||
### Yapılandırma Şablonu
|
||||
|
||||
```sql
|
||||
-- Bağlantı limitleri (RAM için ayarla)
|
||||
ALTER SYSTEM SET max_connections = 100;
|
||||
ALTER SYSTEM SET work_mem = '8MB';
|
||||
|
||||
-- Timeout'lar
|
||||
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
|
||||
ALTER SYSTEM SET statement_timeout = '30s';
|
||||
|
||||
-- İzleme
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
|
||||
-- Güvenlik varsayılanları
|
||||
REVOKE ALL ON SCHEMA public FROM public;
|
||||
|
||||
SELECT pg_reload_conf();
|
||||
```
|
||||
|
||||
## İlgili
|
||||
|
||||
- Agent: `database-reviewer` - Tam veritabanı inceleme iş akışı
|
||||
- Skill: `clickhouse-io` - ClickHouse analytics kalıpları
|
||||
- Skill: `backend-patterns` - API ve backend kalıpları
|
||||
|
||||
---
|
||||
|
||||
*Supabase Agent Skills'e dayanır (kredi: Supabase ekibi) (MIT License)*
|
||||
750
docs/tr/skills/python-patterns/SKILL.md
Normal file
750
docs/tr/skills/python-patterns/SKILL.md
Normal file
@@ -0,0 +1,750 @@
|
||||
---
|
||||
name: python-patterns
|
||||
description: Pythonic idiomlar, PEP 8 standartları, type hint'ler ve sağlam, verimli ve bakımı kolay Python uygulamaları oluşturmak için en iyi uygulamalar.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Python Geliştirme Desenleri
|
||||
|
||||
Sağlam, verimli ve bakımı kolay uygulamalar oluşturmak için idiomatic Python desenleri ve en iyi uygulamalar.
|
||||
|
||||
## Ne Zaman Etkinleştirmeli
|
||||
|
||||
- Yeni Python kodu yazarken
|
||||
- Python kodunu gözden geçirirken
|
||||
- Mevcut Python kodunu refactor ederken
|
||||
- Python paketleri/modülleri tasarlarken
|
||||
|
||||
## Temel Prensipler
|
||||
|
||||
### 1. Okunabilirlik Önemlidir
|
||||
|
||||
Python okunabilirliği önceliklendirir. Kod açık ve anlaşılması kolay olmalıdır.
|
||||
|
||||
```python
|
||||
# İyi: Açık ve okunabilir
|
||||
def get_active_users(users: list[User]) -> list[User]:
|
||||
"""Sağlanan listeden sadece aktif kullanıcıları döndür."""
|
||||
return [user for user in users if user.is_active]
|
||||
|
||||
|
||||
# Kötü: Zeki ama kafa karıştırıcı
|
||||
def get_active_users(u):
|
||||
return [x for x in u if x.a]
|
||||
```
|
||||
|
||||
### 2. Açık, Örtük Olandan Daha İyidir
|
||||
|
||||
Sihirden kaçının; kodunuzun ne yaptığı konusunda açık olun.
|
||||
|
||||
```python
|
||||
# İyi: Açık yapılandırma
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Kötü: Gizli yan etkiler
|
||||
import some_module
|
||||
some_module.setup() # Bu ne yapıyor?
|
||||
```
|
||||
|
||||
### 3. EAFP - Affederek Sormaktansa İzin İstemek Daha Kolaydır
|
||||
|
||||
Python, koşulları kontrol etmek yerine exception handling'i tercih eder.
|
||||
|
||||
```python
|
||||
# İyi: EAFP stili
|
||||
def get_value(dictionary: dict, key: str) -> Any:
|
||||
try:
|
||||
return dictionary[key]
|
||||
except KeyError:
|
||||
return default_value
|
||||
|
||||
# Kötü: LBYL (Atlamadan Önce Bak) stili
|
||||
def get_value(dictionary: dict, key: str) -> Any:
|
||||
if key in dictionary:
|
||||
return dictionary[key]
|
||||
else:
|
||||
return default_value
|
||||
```
|
||||
|
||||
## Type Hint'ler
|
||||
|
||||
### Temel Type Annotation'lar
|
||||
|
||||
```python
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
def process_user(
|
||||
user_id: str,
|
||||
data: Dict[str, Any],
|
||||
active: bool = True
|
||||
) -> Optional[User]:
|
||||
"""Bir kullanıcıyı işle ve güncellenmiş User'ı veya None döndür."""
|
||||
if not active:
|
||||
return None
|
||||
return User(user_id, data)
|
||||
```
|
||||
|
||||
### Modern Type Hint'ler (Python 3.9+)
|
||||
|
||||
```python
|
||||
# Python 3.9+ - Built-in tipleri kullan
|
||||
def process_items(items: list[str]) -> dict[str, int]:
|
||||
return {item: len(item) for item in items}
|
||||
|
||||
# Python 3.8 ve öncesi - typing modülünü kullan
|
||||
from typing import List, Dict
|
||||
|
||||
def process_items(items: List[str]) -> Dict[str, int]:
|
||||
return {item: len(item) for item in items}
|
||||
```
|
||||
|
||||
### Type Alias'ları ve TypeVar
|
||||
|
||||
```python
|
||||
from typing import TypeVar, Union
|
||||
|
||||
# Karmaşık tipler için type alias
|
||||
JSON = Union[dict[str, Any], list[Any], str, int, float, bool, None]
|
||||
|
||||
def parse_json(data: str) -> JSON:
|
||||
return json.loads(data)
|
||||
|
||||
# Generic tipler
|
||||
T = TypeVar('T')
|
||||
|
||||
def first(items: list[T]) -> T | None:
|
||||
"""İlk öğeyi döndür veya liste boşsa None döndür."""
|
||||
return items[0] if items else None
|
||||
```
|
||||
|
||||
### Protocol Tabanlı Duck Typing
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class Renderable(Protocol):
|
||||
def render(self) -> str:
|
||||
"""Nesneyi string'e render et."""
|
||||
|
||||
def render_all(items: list[Renderable]) -> str:
|
||||
"""Renderable protocol'ünü implement eden tüm öğeleri render et."""
|
||||
return "\n".join(item.render() for item in items)
|
||||
```
|
||||
|
||||
## Hata İşleme Desenleri
|
||||
|
||||
### Spesifik Exception Handling
|
||||
|
||||
```python
|
||||
# İyi: Spesifik exception'ları yakala
|
||||
def load_config(path: str) -> Config:
|
||||
try:
|
||||
with open(path) as f:
|
||||
return Config.from_json(f.read())
|
||||
except FileNotFoundError as e:
|
||||
raise ConfigError(f"Config file not found: {path}") from e
|
||||
except json.JSONDecodeError as e:
|
||||
raise ConfigError(f"Invalid JSON in config: {path}") from e
|
||||
|
||||
# Kötü: Bare except
|
||||
def load_config(path: str) -> Config:
|
||||
try:
|
||||
with open(path) as f:
|
||||
return Config.from_json(f.read())
|
||||
except:
|
||||
return None # Sessiz hata!
|
||||
```
|
||||
|
||||
### Exception Chaining
|
||||
|
||||
```python
|
||||
def process_data(data: str) -> Result:
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
except json.JSONDecodeError as e:
|
||||
# Traceback'i korumak için exception'ları zincirleme
|
||||
raise ValueError(f"Failed to parse data: {data}") from e
|
||||
```
|
||||
|
||||
### Özel Exception Hiyerarşisi
|
||||
|
||||
```python
|
||||
class AppError(Exception):
|
||||
"""Tüm uygulama hataları için base exception."""
|
||||
pass
|
||||
|
||||
class ValidationError(AppError):
|
||||
"""Input validation başarısız olduğunda raise edilir."""
|
||||
pass
|
||||
|
||||
class NotFoundError(AppError):
|
||||
"""İstenen kaynak bulunamadığında raise edilir."""
|
||||
pass
|
||||
|
||||
# Kullanım
|
||||
def get_user(user_id: str) -> User:
|
||||
user = db.find_user(user_id)
|
||||
if not user:
|
||||
raise NotFoundError(f"User not found: {user_id}")
|
||||
return user
|
||||
```
|
||||
|
||||
## Context Manager'lar
|
||||
|
||||
### Kaynak Yönetimi
|
||||
|
||||
```python
|
||||
# İyi: Context manager'ları kullanma
|
||||
def process_file(path: str) -> str:
|
||||
with open(path, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
# Kötü: Manuel kaynak yönetimi
|
||||
def process_file(path: str) -> str:
|
||||
f = open(path, 'r')
|
||||
try:
|
||||
return f.read()
|
||||
finally:
|
||||
f.close()
|
||||
```
|
||||
|
||||
### Özel Context Manager'lar
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def timer(name: str):
|
||||
"""Bir kod bloğunu zamanlamak için context manager."""
|
||||
start = time.perf_counter()
|
||||
yield
|
||||
elapsed = time.perf_counter() - start
|
||||
print(f"{name} took {elapsed:.4f} seconds")
|
||||
|
||||
# Kullanım
|
||||
with timer("data processing"):
|
||||
process_large_dataset()
|
||||
```
|
||||
|
||||
### Context Manager Class'ları
|
||||
|
||||
```python
|
||||
class DatabaseTransaction:
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def __enter__(self):
|
||||
self.connection.begin_transaction()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is None:
|
||||
self.connection.commit()
|
||||
else:
|
||||
self.connection.rollback()
|
||||
return False # Exception'ları suppress etme
|
||||
|
||||
# Kullanım
|
||||
with DatabaseTransaction(conn):
|
||||
user = conn.create_user(user_data)
|
||||
conn.create_profile(user.id, profile_data)
|
||||
```
|
||||
|
||||
## Comprehension'lar ve Generator'lar
|
||||
|
||||
### List Comprehension'ları
|
||||
|
||||
```python
|
||||
# İyi: Basit dönüşümler için list comprehension
|
||||
names = [user.name for user in users if user.is_active]
|
||||
|
||||
# Kötü: Manuel döngü
|
||||
names = []
|
||||
for user in users:
|
||||
if user.is_active:
|
||||
names.append(user.name)
|
||||
|
||||
# Karmaşık comprehension'lar genişletilmelidir
|
||||
# Kötü: Çok karmaşık
|
||||
result = [x * 2 for x in items if x > 0 if x % 2 == 0]
|
||||
|
||||
# İyi: Bir generator fonksiyonu kullan
|
||||
def filter_and_transform(items: Iterable[int]) -> list[int]:
|
||||
result = []
|
||||
for x in items:
|
||||
if x > 0 and x % 2 == 0:
|
||||
result.append(x * 2)
|
||||
return result
|
||||
```
|
||||
|
||||
### Generator Expression'ları
|
||||
|
||||
```python
|
||||
# İyi: Lazy evaluation için generator
|
||||
total = sum(x * x for x in range(1_000_000))
|
||||
|
||||
# Kötü: Büyük ara liste oluşturur
|
||||
total = sum([x * x for x in range(1_000_000)])
|
||||
```
|
||||
|
||||
### Generator Fonksiyonları
|
||||
|
||||
```python
|
||||
def read_large_file(path: str) -> Iterator[str]:
|
||||
"""Büyük bir dosyayı satır satır oku."""
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
yield line.strip()
|
||||
|
||||
# Kullanım
|
||||
for line in read_large_file("huge.txt"):
|
||||
process(line)
|
||||
```
|
||||
|
||||
## Data Class'lar ve Named Tuple'lar
|
||||
|
||||
### Data Class'lar
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""Otomatik __init__, __repr__ ve __eq__ ile User entity."""
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
is_active: bool = True
|
||||
|
||||
# Kullanım
|
||||
user = User(
|
||||
id="123",
|
||||
name="Alice",
|
||||
email="alice@example.com"
|
||||
)
|
||||
```
|
||||
|
||||
### Validation ile Data Class'lar
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class User:
|
||||
email: str
|
||||
age: int
|
||||
|
||||
def __post_init__(self):
|
||||
# Email formatını validate et
|
||||
if "@" not in self.email:
|
||||
raise ValueError(f"Invalid email: {self.email}")
|
||||
# Yaş aralığını validate et
|
||||
if self.age < 0 or self.age > 150:
|
||||
raise ValueError(f"Invalid age: {self.age}")
|
||||
```
|
||||
|
||||
### Named Tuple'lar
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
|
||||
class Point(NamedTuple):
|
||||
"""Immutable 2D nokta."""
|
||||
x: float
|
||||
y: float
|
||||
|
||||
def distance(self, other: 'Point') -> float:
|
||||
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
|
||||
|
||||
# Kullanım
|
||||
p1 = Point(0, 0)
|
||||
p2 = Point(3, 4)
|
||||
print(p1.distance(p2)) # 5.0
|
||||
```
|
||||
|
||||
## Decorator'lar
|
||||
|
||||
### Fonksiyon Decorator'ları
|
||||
|
||||
```python
|
||||
import functools
|
||||
import time
|
||||
|
||||
def timer(func: Callable) -> Callable:
|
||||
"""Fonksiyon yürütmesini zamanlamak için decorator."""
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start = time.perf_counter()
|
||||
result = func(*args, **kwargs)
|
||||
elapsed = time.perf_counter() - start
|
||||
print(f"{func.__name__} took {elapsed:.4f}s")
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
@timer
|
||||
def slow_function():
|
||||
time.sleep(1)
|
||||
|
||||
# slow_function() yazdırır: slow_function took 1.0012s
|
||||
```
|
||||
|
||||
### Parametreli Decorator'lar
|
||||
|
||||
```python
|
||||
def repeat(times: int):
|
||||
"""Bir fonksiyonu birden çok kez tekrarlamak için decorator."""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
results = []
|
||||
for _ in range(times):
|
||||
results.append(func(*args, **kwargs))
|
||||
return results
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@repeat(times=3)
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# greet("Alice") döndürür ["Hello, Alice!", "Hello, Alice!", "Hello, Alice!"]
|
||||
```
|
||||
|
||||
### Class Tabanlı Decorator'lar
|
||||
|
||||
```python
|
||||
class CountCalls:
|
||||
"""Bir fonksiyonun kaç kez çağrıldığını sayan decorator."""
|
||||
def __init__(self, func: Callable):
|
||||
functools.update_wrapper(self, func)
|
||||
self.func = func
|
||||
self.count = 0
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.count += 1
|
||||
print(f"{self.func.__name__} has been called {self.count} times")
|
||||
return self.func(*args, **kwargs)
|
||||
|
||||
@CountCalls
|
||||
def process():
|
||||
pass
|
||||
|
||||
# Her process() çağrısı çağrı sayısını yazdırır
|
||||
```
|
||||
|
||||
## Eşzamanlılık Desenleri
|
||||
|
||||
### I/O-Bound Görevler için Threading
|
||||
|
||||
```python
|
||||
import concurrent.futures
|
||||
import threading
|
||||
|
||||
def fetch_url(url: str) -> str:
|
||||
"""Bir URL fetch et (I/O-bound operasyon)."""
|
||||
import urllib.request
|
||||
with urllib.request.urlopen(url) as response:
|
||||
return response.read().decode()
|
||||
|
||||
def fetch_all_urls(urls: list[str]) -> dict[str, str]:
|
||||
"""Thread'ler kullanarak birden fazla URL'yi eşzamanlı fetch et."""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
future_to_url = {executor.submit(fetch_url, url): url for url in urls}
|
||||
results = {}
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
results[url] = future.result()
|
||||
except Exception as e:
|
||||
results[url] = f"Error: {e}"
|
||||
return results
|
||||
```
|
||||
|
||||
### CPU-Bound Görevler için Multiprocessing
|
||||
|
||||
```python
|
||||
def process_data(data: list[int]) -> int:
|
||||
"""CPU-yoğun hesaplama."""
|
||||
return sum(x ** 2 for x in data)
|
||||
|
||||
def process_all(datasets: list[list[int]]) -> list[int]:
|
||||
"""Birden fazla process kullanarak birden fazla dataset işle."""
|
||||
with concurrent.futures.ProcessPoolExecutor() as executor:
|
||||
results = list(executor.map(process_data, datasets))
|
||||
return results
|
||||
```
|
||||
|
||||
### Eşzamanlı I/O için Async/Await
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def fetch_async(url: str) -> str:
|
||||
"""Asenkron olarak bir URL fetch et."""
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.text()
|
||||
|
||||
async def fetch_all(urls: list[str]) -> dict[str, str]:
|
||||
"""Birden fazla URL'yi eşzamanlı fetch et."""
|
||||
tasks = [fetch_async(url) for url in urls]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
return dict(zip(urls, results))
|
||||
```
|
||||
|
||||
## Paket Organizasyonu
|
||||
|
||||
### Standart Proje Düzeni
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── src/
|
||||
│ └── mypackage/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── routes.py
|
||||
│ ├── models/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── user.py
|
||||
│ └── utils/
|
||||
│ ├── __init__.py
|
||||
│ └── helpers.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py
|
||||
│ ├── test_api.py
|
||||
│ └── test_models.py
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
### Import Konvansiyonları
|
||||
|
||||
```python
|
||||
# İyi: Import sırası - stdlib, third-party, local
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from fastapi import FastAPI
|
||||
|
||||
from mypackage.models import User
|
||||
from mypackage.utils import format_name
|
||||
|
||||
# İyi: Otomatik import sıralama için isort kullanın
|
||||
# pip install isort
|
||||
```
|
||||
|
||||
### Paket Export'ları için __init__.py
|
||||
|
||||
```python
|
||||
# mypackage/__init__.py
|
||||
"""mypackage - Örnek bir Python paketi."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
# Ana class/fonksiyonları paket seviyesinde export et
|
||||
from mypackage.models import User, Post
|
||||
from mypackage.utils import format_name
|
||||
|
||||
__all__ = ["User", "Post", "format_name"]
|
||||
```
|
||||
|
||||
## Bellek ve Performans
|
||||
|
||||
### Bellek Verimliliği için __slots__ Kullanma
|
||||
|
||||
```python
|
||||
# Kötü: Normal class __dict__ kullanır (daha fazla bellek)
|
||||
class Point:
|
||||
def __init__(self, x: float, y: float):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
# İyi: __slots__ bellek kullanımını azaltır
|
||||
class Point:
|
||||
__slots__ = ['x', 'y']
|
||||
|
||||
def __init__(self, x: float, y: float):
|
||||
self.x = x
|
||||
self.y = y
|
||||
```
|
||||
|
||||
### Büyük Veri için Generator
|
||||
|
||||
```python
|
||||
# Kötü: Bellekte tam liste döndürür
|
||||
def read_lines(path: str) -> list[str]:
|
||||
with open(path) as f:
|
||||
return [line.strip() for line in f]
|
||||
|
||||
# İyi: Satırları birer birer yield eder
|
||||
def read_lines(path: str) -> Iterator[str]:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
yield line.strip()
|
||||
```
|
||||
|
||||
### Döngülerde String Birleştirmekten Kaçının
|
||||
|
||||
```python
|
||||
# Kötü: String immutability nedeniyle O(n²)
|
||||
result = ""
|
||||
for item in items:
|
||||
result += str(item)
|
||||
|
||||
# İyi: join kullanarak O(n)
|
||||
result = "".join(str(item) for item in items)
|
||||
|
||||
# İyi: Oluşturma için StringIO kullanma
|
||||
from io import StringIO
|
||||
|
||||
buffer = StringIO()
|
||||
for item in items:
|
||||
buffer.write(str(item))
|
||||
result = buffer.getvalue()
|
||||
```
|
||||
|
||||
## Python Tooling Entegrasyonu
|
||||
|
||||
### Temel Komutlar
|
||||
|
||||
```bash
|
||||
# Kod formatlama
|
||||
black .
|
||||
isort .
|
||||
|
||||
# Linting
|
||||
ruff check .
|
||||
pylint mypackage/
|
||||
|
||||
# Type checking
|
||||
mypy .
|
||||
|
||||
# Test
|
||||
pytest --cov=mypackage --cov-report=html
|
||||
|
||||
# Güvenlik taraması
|
||||
bandit -r .
|
||||
|
||||
# Dependency yönetimi
|
||||
pip-audit
|
||||
safety check
|
||||
```
|
||||
|
||||
### pyproject.toml Yapılandırması
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "mypackage"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"requests>=2.31.0",
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.4.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"black>=23.0.0",
|
||||
"ruff>=0.1.0",
|
||||
"mypy>=1.5.0",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py39']
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
select = ["E", "F", "I", "N", "W"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "--cov=mypackage --cov-report=term-missing"
|
||||
```
|
||||
|
||||
## Hızlı Referans: Python İfadeleri
|
||||
|
||||
| İfade | Açıklama |
|
||||
|-------|----------|
|
||||
| EAFP | Affederek Sormaktansa İzin İstemek Daha Kolay |
|
||||
| Context manager'lar | Kaynak yönetimi için `with` kullan |
|
||||
| List comprehension'lar | Basit dönüşümler için |
|
||||
| Generator'lar | Lazy evaluation ve büyük dataset'ler için |
|
||||
| Type hint'ler | Fonksiyon signature'larını annotate et |
|
||||
| Dataclass'lar | Auto-generated metodlarla veri container'ları için |
|
||||
| `__slots__` | Bellek optimizasyonu için |
|
||||
| f-string'ler | String formatlama için (Python 3.6+) |
|
||||
| `pathlib.Path` | Path operasyonları için (Python 3.4+) |
|
||||
| `enumerate` | Döngülerde index-element çiftleri için |
|
||||
|
||||
## Kaçınılması Gereken Anti-Desenler
|
||||
|
||||
```python
|
||||
# Kötü: Mutable default argümanlar
|
||||
def append_to(item, items=[]):
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
# İyi: None kullan ve yeni liste oluştur
|
||||
def append_to(item, items=None):
|
||||
if items is None:
|
||||
items = []
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
# Kötü: type() ile tip kontrolü
|
||||
if type(obj) == list:
|
||||
process(obj)
|
||||
|
||||
# İyi: isinstance kullan
|
||||
if isinstance(obj, list):
|
||||
process(obj)
|
||||
|
||||
# Kötü: None ile == ile karşılaştırma
|
||||
if value == None:
|
||||
process()
|
||||
|
||||
# İyi: is kullan
|
||||
if value is None:
|
||||
process()
|
||||
|
||||
# Kötü: from module import *
|
||||
from os.path import *
|
||||
|
||||
# İyi: Açık import'lar
|
||||
from os.path import join, exists
|
||||
|
||||
# Kötü: Bare except
|
||||
try:
|
||||
risky_operation()
|
||||
except:
|
||||
pass
|
||||
|
||||
# İyi: Spesifik exception
|
||||
try:
|
||||
risky_operation()
|
||||
except SpecificError as e:
|
||||
logger.error(f"Operation failed: {e}")
|
||||
```
|
||||
|
||||
__Unutmayın__: Python kodu okunabilir, açık ve en az sürpriz ilkesine uygun olmalıdır. Şüphe duyduğunuzda, açıklığı zekiceden öncelikli kılın.
|
||||
816
docs/tr/skills/python-testing/SKILL.md
Normal file
816
docs/tr/skills/python-testing/SKILL.md
Normal file
@@ -0,0 +1,816 @@
|
||||
---
|
||||
name: python-testing
|
||||
description: pytest, TDD metodolojisi, fixture'lar, mocking, parametrizasyon ve coverage gereksinimleri kullanarak Python test stratejileri.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Python Test Desenleri
|
||||
|
||||
pytest, TDD metodolojisi ve en iyi uygulamalar kullanarak Python uygulamaları için kapsamlı test stratejileri.
|
||||
|
||||
## Ne Zaman Etkinleştirmeli
|
||||
|
||||
- Yeni Python kodu yazarken (TDD'yi takip et: red, green, refactor)
|
||||
- Python projeleri için test suite'leri tasarlarken
|
||||
- Python test coverage'ını gözden geçirirken
|
||||
- Test altyapısını kurarken
|
||||
|
||||
## Temel Test Felsefesi
|
||||
|
||||
### Test-Driven Development (TDD)
|
||||
|
||||
Her zaman TDD döngüsünü takip edin:
|
||||
|
||||
1. **RED**: İstenen davranış için başarısız bir test yaz
|
||||
2. **GREEN**: Testi geçirmek için minimal kod yaz
|
||||
3. **REFACTOR**: Testleri yeşil tutarken kodu iyileştir
|
||||
|
||||
```python
|
||||
# Adım 1: Başarısız test yaz (RED)
|
||||
def test_add_numbers():
|
||||
result = add(2, 3)
|
||||
assert result == 5
|
||||
|
||||
# Adım 2: Minimal implementasyon yaz (GREEN)
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
# Adım 3: Gerekirse refactor et (REFACTOR)
|
||||
```
|
||||
|
||||
### Coverage Gereksinimleri
|
||||
|
||||
- **Hedef**: 80%+ kod coverage'ı
|
||||
- **Kritik yollar**: 100% coverage gereklidir
|
||||
- Coverage'ı ölçmek için `pytest --cov` kullanın
|
||||
|
||||
```bash
|
||||
pytest --cov=mypackage --cov-report=term-missing --cov-report=html
|
||||
```
|
||||
|
||||
## pytest Temelleri
|
||||
|
||||
### Temel Test Yapısı
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
def test_addition():
|
||||
"""Temel toplama testi."""
|
||||
assert 2 + 2 == 4
|
||||
|
||||
def test_string_uppercase():
|
||||
"""String büyük harf yapma testi."""
|
||||
text = "hello"
|
||||
assert text.upper() == "HELLO"
|
||||
|
||||
def test_list_append():
|
||||
"""Liste append testi."""
|
||||
items = [1, 2, 3]
|
||||
items.append(4)
|
||||
assert 4 in items
|
||||
assert len(items) == 4
|
||||
```
|
||||
|
||||
### Assertion'lar
|
||||
|
||||
```python
|
||||
# Eşitlik
|
||||
assert result == expected
|
||||
|
||||
# Eşitsizlik
|
||||
assert result != unexpected
|
||||
|
||||
# Doğruluk değeri
|
||||
assert result # Truthy
|
||||
assert not result # Falsy
|
||||
assert result is True # Tam olarak True
|
||||
assert result is False # Tam olarak False
|
||||
assert result is None # Tam olarak None
|
||||
|
||||
# Üyelik
|
||||
assert item in collection
|
||||
assert item not in collection
|
||||
|
||||
# Karşılaştırmalar
|
||||
assert result > 0
|
||||
assert 0 <= result <= 100
|
||||
|
||||
# Tip kontrolü
|
||||
assert isinstance(result, str)
|
||||
|
||||
# Exception testi (tercih edilen yaklaşım)
|
||||
with pytest.raises(ValueError):
|
||||
raise ValueError("error message")
|
||||
|
||||
# Exception mesajını kontrol et
|
||||
with pytest.raises(ValueError, match="invalid input"):
|
||||
raise ValueError("invalid input provided")
|
||||
|
||||
# Exception niteliklerini kontrol et
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
raise ValueError("error message")
|
||||
assert str(exc_info.value) == "error message"
|
||||
```
|
||||
|
||||
## Fixture'lar
|
||||
|
||||
### Temel Fixture Kullanımı
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def sample_data():
|
||||
"""Örnek veri sağlayan fixture."""
|
||||
return {"name": "Alice", "age": 30}
|
||||
|
||||
def test_sample_data(sample_data):
|
||||
"""Fixture kullanan test."""
|
||||
assert sample_data["name"] == "Alice"
|
||||
assert sample_data["age"] == 30
|
||||
```
|
||||
|
||||
### Setup/Teardown ile Fixture
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def database():
|
||||
"""Setup ve teardown ile fixture."""
|
||||
# Setup
|
||||
db = Database(":memory:")
|
||||
db.create_tables()
|
||||
db.insert_test_data()
|
||||
|
||||
yield db # Teste sağla
|
||||
|
||||
# Teardown
|
||||
db.close()
|
||||
|
||||
def test_database_query(database):
|
||||
"""Veritabanı operasyonlarını test et."""
|
||||
result = database.query("SELECT * FROM users")
|
||||
assert len(result) > 0
|
||||
```
|
||||
|
||||
### Fixture Scope'ları
|
||||
|
||||
```python
|
||||
# Function scope (varsayılan) - her test için çalışır
|
||||
@pytest.fixture
|
||||
def temp_file():
|
||||
with open("temp.txt", "w") as f:
|
||||
yield f
|
||||
os.remove("temp.txt")
|
||||
|
||||
# Module scope - modül başına bir kez çalışır
|
||||
@pytest.fixture(scope="module")
|
||||
def module_db():
|
||||
db = Database(":memory:")
|
||||
db.create_tables()
|
||||
yield db
|
||||
db.close()
|
||||
|
||||
# Session scope - test oturumu başına bir kez çalışır
|
||||
@pytest.fixture(scope="session")
|
||||
def shared_resource():
|
||||
resource = ExpensiveResource()
|
||||
yield resource
|
||||
resource.cleanup()
|
||||
```
|
||||
|
||||
### Parametreli Fixture
|
||||
|
||||
```python
|
||||
@pytest.fixture(params=[1, 2, 3])
|
||||
def number(request):
|
||||
"""Parametreli fixture."""
|
||||
return request.param
|
||||
|
||||
def test_numbers(number):
|
||||
"""Test her parametre için 3 kez çalışır."""
|
||||
assert number > 0
|
||||
```
|
||||
|
||||
### Birden Fazla Fixture Kullanma
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def user():
|
||||
return User(id=1, name="Alice")
|
||||
|
||||
@pytest.fixture
|
||||
def admin():
|
||||
return User(id=2, name="Admin", role="admin")
|
||||
|
||||
def test_user_admin_interaction(user, admin):
|
||||
"""Birden fazla fixture kullanan test."""
|
||||
assert admin.can_manage(user)
|
||||
```
|
||||
|
||||
### Autouse Fixture'ları
|
||||
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_config():
|
||||
"""Her testten önce otomatik olarak çalışır."""
|
||||
Config.reset()
|
||||
yield
|
||||
Config.cleanup()
|
||||
|
||||
def test_without_fixture_call():
|
||||
# reset_config otomatik olarak çalışır
|
||||
assert Config.get_setting("debug") is False
|
||||
```
|
||||
|
||||
### Paylaşılan Fixture'lar için Conftest.py
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Tüm testler için paylaşılan fixture."""
|
||||
app = create_app(testing=True)
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers(client):
|
||||
"""API testi için auth header'ları oluştur."""
|
||||
response = client.post("/api/login", json={
|
||||
"username": "test",
|
||||
"password": "test"
|
||||
})
|
||||
token = response.json["token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
```
|
||||
|
||||
## Parametrizasyon
|
||||
|
||||
### Temel Parametrizasyon
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("input,expected", [
|
||||
("hello", "HELLO"),
|
||||
("world", "WORLD"),
|
||||
("PyThOn", "PYTHON"),
|
||||
])
|
||||
def test_uppercase(input, expected):
|
||||
"""Test farklı input'larla 3 kez çalışır."""
|
||||
assert input.upper() == expected
|
||||
```
|
||||
|
||||
### Birden Fazla Parametre
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("a,b,expected", [
|
||||
(2, 3, 5),
|
||||
(0, 0, 0),
|
||||
(-1, 1, 0),
|
||||
(100, 200, 300),
|
||||
])
|
||||
def test_add(a, b, expected):
|
||||
"""Birden fazla input ile toplama testi."""
|
||||
assert add(a, b) == expected
|
||||
```
|
||||
|
||||
### ID'li Parametrizasyon
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("input,expected", [
|
||||
("valid@email.com", True),
|
||||
("invalid", False),
|
||||
("@no-domain.com", False),
|
||||
], ids=["valid-email", "missing-at", "missing-domain"])
|
||||
def test_email_validation(input, expected):
|
||||
"""Okunabilir test ID'leri ile email validation testi."""
|
||||
assert is_valid_email(input) is expected
|
||||
```
|
||||
|
||||
### Parametreli Fixture'lar
|
||||
|
||||
```python
|
||||
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
|
||||
def db(request):
|
||||
"""Birden fazla veritabanı backend'ine karşı test."""
|
||||
if request.param == "sqlite":
|
||||
return Database(":memory:")
|
||||
elif request.param == "postgresql":
|
||||
return Database("postgresql://localhost/test")
|
||||
elif request.param == "mysql":
|
||||
return Database("mysql://localhost/test")
|
||||
|
||||
def test_database_operations(db):
|
||||
"""Test her veritabanı için 3 kez çalışır."""
|
||||
result = db.query("SELECT 1")
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
## Marker'lar ve Test Seçimi
|
||||
|
||||
### Özel Marker'lar
|
||||
|
||||
```python
|
||||
# Yavaş testleri işaretle
|
||||
@pytest.mark.slow
|
||||
def test_slow_operation():
|
||||
time.sleep(5)
|
||||
|
||||
# Entegrasyon testlerini işaretle
|
||||
@pytest.mark.integration
|
||||
def test_api_integration():
|
||||
response = requests.get("https://api.example.com")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Unit testleri işaretle
|
||||
@pytest.mark.unit
|
||||
def test_unit_logic():
|
||||
assert calculate(2, 3) == 5
|
||||
```
|
||||
|
||||
### Belirli Testleri Çalıştırma
|
||||
|
||||
```bash
|
||||
# Sadece hızlı testleri çalıştır
|
||||
pytest -m "not slow"
|
||||
|
||||
# Sadece entegrasyon testlerini çalıştır
|
||||
pytest -m integration
|
||||
|
||||
# Entegrasyon veya yavaş testleri çalıştır
|
||||
pytest -m "integration or slow"
|
||||
|
||||
# Unit olarak işaretlenmiş ama yavaş olmayan testleri çalıştır
|
||||
pytest -m "unit and not slow"
|
||||
```
|
||||
|
||||
### pytest.ini'de Marker'ları Yapılandırma
|
||||
|
||||
```ini
|
||||
[pytest]
|
||||
markers =
|
||||
slow: marks tests as slow
|
||||
integration: marks tests as integration tests
|
||||
unit: marks tests as unit tests
|
||||
django: marks tests as requiring Django
|
||||
```
|
||||
|
||||
## Mocking ve Patching
|
||||
|
||||
### Fonksiyonları Mocking
|
||||
|
||||
```python
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
@patch("mypackage.external_api_call")
|
||||
def test_with_mock(api_call_mock):
|
||||
"""Mock'lanmış harici API ile test."""
|
||||
api_call_mock.return_value = {"status": "success"}
|
||||
|
||||
result = my_function()
|
||||
|
||||
api_call_mock.assert_called_once()
|
||||
assert result["status"] == "success"
|
||||
```
|
||||
|
||||
### Dönüş Değerlerini Mocking
|
||||
|
||||
```python
|
||||
@patch("mypackage.Database.connect")
|
||||
def test_database_connection(connect_mock):
|
||||
"""Mock'lanmış veritabanı bağlantısı ile test."""
|
||||
connect_mock.return_value = MockConnection()
|
||||
|
||||
db = Database()
|
||||
db.connect()
|
||||
|
||||
connect_mock.assert_called_once_with("localhost")
|
||||
```
|
||||
|
||||
### Exception'ları Mocking
|
||||
|
||||
```python
|
||||
@patch("mypackage.api_call")
|
||||
def test_api_error_handling(api_call_mock):
|
||||
"""Mock'lanmış exception ile hata işleme testi."""
|
||||
api_call_mock.side_effect = ConnectionError("Network error")
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
api_call()
|
||||
|
||||
api_call_mock.assert_called_once()
|
||||
```
|
||||
|
||||
### Context Manager'ları Mocking
|
||||
|
||||
```python
|
||||
@patch("builtins.open", new_callable=mock_open)
|
||||
def test_file_reading(mock_file):
|
||||
"""Mock'lanmış open ile dosya okuma testi."""
|
||||
mock_file.return_value.read.return_value = "file content"
|
||||
|
||||
result = read_file("test.txt")
|
||||
|
||||
mock_file.assert_called_once_with("test.txt", "r")
|
||||
assert result == "file content"
|
||||
```
|
||||
|
||||
### Autospec Kullanma
|
||||
|
||||
```python
|
||||
@patch("mypackage.DBConnection", autospec=True)
|
||||
def test_autospec(db_mock):
|
||||
"""API yanlış kullanımını yakalamak için autospec ile test."""
|
||||
db = db_mock.return_value
|
||||
db.query("SELECT * FROM users")
|
||||
|
||||
# DBConnection query metodu yoksa bu başarısız olur
|
||||
db_mock.assert_called_once()
|
||||
```
|
||||
|
||||
### Mock Class Instance'ları
|
||||
|
||||
```python
|
||||
class TestUserService:
|
||||
@patch("mypackage.UserRepository")
|
||||
def test_create_user(self, repo_mock):
|
||||
"""Mock'lanmış repository ile kullanıcı oluşturma testi."""
|
||||
repo_mock.return_value.save.return_value = User(id=1, name="Alice")
|
||||
|
||||
service = UserService(repo_mock.return_value)
|
||||
user = service.create_user(name="Alice")
|
||||
|
||||
assert user.name == "Alice"
|
||||
repo_mock.return_value.save.assert_called_once()
|
||||
```
|
||||
|
||||
### Mock Property
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Property'li bir mock oluştur."""
|
||||
config = Mock()
|
||||
type(config).debug = PropertyMock(return_value=True)
|
||||
type(config).api_key = PropertyMock(return_value="test-key")
|
||||
return config
|
||||
|
||||
def test_with_mock_config(mock_config):
|
||||
"""Mock'lanmış config property'leri ile test."""
|
||||
assert mock_config.debug is True
|
||||
assert mock_config.api_key == "test-key"
|
||||
```
|
||||
|
||||
## Asenkron Kodu Test Etme
|
||||
|
||||
### pytest-asyncio ile Asenkron Testler
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_function():
|
||||
"""Asenkron fonksiyon testi."""
|
||||
result = await async_add(2, 3)
|
||||
assert result == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_with_fixture(async_client):
|
||||
"""Asenkron fixture ile asenkron test."""
|
||||
response = await async_client.get("/api/users")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
### Asenkron Fixture
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
async def async_client():
|
||||
"""Asenkron test client sağlayan asenkron fixture."""
|
||||
app = create_app()
|
||||
async with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_endpoint(async_client):
|
||||
"""Asenkron fixture kullanan test."""
|
||||
response = await async_client.get("/api/data")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
### Asenkron Fonksiyonları Mocking
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
@patch("mypackage.async_api_call")
|
||||
async def test_async_mock(api_call_mock):
|
||||
"""Mock ile asenkron fonksiyon testi."""
|
||||
api_call_mock.return_value = {"status": "ok"}
|
||||
|
||||
result = await my_async_function()
|
||||
|
||||
api_call_mock.assert_awaited_once()
|
||||
assert result["status"] == "ok"
|
||||
```
|
||||
|
||||
## Exception'ları Test Etme
|
||||
|
||||
### Beklenen Exception'ları Test Etme
|
||||
|
||||
```python
|
||||
def test_divide_by_zero():
|
||||
"""Sıfıra bölmenin ZeroDivisionError raise ettiğini test et."""
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
divide(10, 0)
|
||||
|
||||
def test_custom_exception():
|
||||
"""Mesaj ile özel exception testi."""
|
||||
with pytest.raises(ValueError, match="invalid input"):
|
||||
validate_input("invalid")
|
||||
```
|
||||
|
||||
### Exception Niteliklerini Test Etme
|
||||
|
||||
```python
|
||||
def test_exception_with_details():
|
||||
"""Özel niteliklerle exception testi."""
|
||||
with pytest.raises(CustomError) as exc_info:
|
||||
raise CustomError("error", code=400)
|
||||
|
||||
assert exc_info.value.code == 400
|
||||
assert "error" in str(exc_info.value)
|
||||
```
|
||||
|
||||
## Yan Etkileri Test Etme
|
||||
|
||||
### Dosya Operasyonlarını Test Etme
|
||||
|
||||
```python
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
def test_file_processing():
|
||||
"""Geçici dosya ile dosya işleme testi."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
||||
f.write("test content")
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = process_file(temp_path)
|
||||
assert result == "processed: test content"
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
```
|
||||
|
||||
### pytest'in tmp_path Fixture'ı ile Test Etme
|
||||
|
||||
```python
|
||||
def test_with_tmp_path(tmp_path):
|
||||
"""pytest'in built-in geçici yol fixture'ını kullanarak test."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("hello world")
|
||||
|
||||
result = process_file(str(test_file))
|
||||
assert result == "hello world"
|
||||
# tmp_path otomatik olarak temizlenir
|
||||
```
|
||||
|
||||
### tmpdir Fixture ile Test Etme
|
||||
|
||||
```python
|
||||
def test_with_tmpdir(tmpdir):
|
||||
"""pytest'in tmpdir fixture'ını kullanarak test."""
|
||||
test_file = tmpdir.join("test.txt")
|
||||
test_file.write("data")
|
||||
|
||||
result = process_file(str(test_file))
|
||||
assert result == "data"
|
||||
```
|
||||
|
||||
## Test Organizasyonu
|
||||
|
||||
### Dizin Yapısı
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # Paylaşılan fixture'lar
|
||||
├── __init__.py
|
||||
├── unit/ # Unit testler
|
||||
│ ├── __init__.py
|
||||
│ ├── test_models.py
|
||||
│ ├── test_utils.py
|
||||
│ └── test_services.py
|
||||
├── integration/ # Entegrasyon testleri
|
||||
│ ├── __init__.py
|
||||
│ ├── test_api.py
|
||||
│ └── test_database.py
|
||||
└── e2e/ # End-to-end testler
|
||||
├── __init__.py
|
||||
└── test_user_flow.py
|
||||
```
|
||||
|
||||
### Test Class'ları
|
||||
|
||||
```python
|
||||
class TestUserService:
|
||||
"""İlgili testleri bir class'ta grupla."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Bu class'taki her testten önce çalışan setup."""
|
||||
self.service = UserService()
|
||||
|
||||
def test_create_user(self):
|
||||
"""Kullanıcı oluşturma testi."""
|
||||
user = self.service.create_user("Alice")
|
||||
assert user.name == "Alice"
|
||||
|
||||
def test_delete_user(self):
|
||||
"""Kullanıcı silme testi."""
|
||||
user = User(id=1, name="Bob")
|
||||
self.service.delete_user(user)
|
||||
assert not self.service.user_exists(1)
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
### YAPIN
|
||||
|
||||
- **TDD'yi takip edin**: Koddan önce testleri yazın (red-green-refactor)
|
||||
- **Bir şeyi test edin**: Her test tek bir davranışı doğrulamalı
|
||||
- **Açıklayıcı isimler kullanın**: `test_user_login_with_invalid_credentials_fails`
|
||||
- **Fixture'ları kullanın**: Tekrarı fixture'larla ortadan kaldırın
|
||||
- **Harici bağımlılıkları mock'layın**: Harici servislere bağımlı olmayın
|
||||
- **Kenar durumları test edin**: Boş input'lar, None değerleri, sınır koşulları
|
||||
- **%80+ coverage hedefleyin**: Kritik yollara odaklanın
|
||||
- **Testleri hızlı tutun**: Yavaş testleri ayırmak için marker'lar kullanın
|
||||
|
||||
### YAPMAYIN
|
||||
|
||||
- **İmplementasyonu test etmeyin**: Davranışı test edin, iç yapıyı değil
|
||||
- **Testlerde karmaşık koşullar kullanmayın**: Testleri basit tutun
|
||||
- **Test hatalarını göz ardı etmeyin**: Tüm testler geçmeli
|
||||
- **Third-party kodu test etmeyin**: Kütüphanelerin çalıştığına güvenin
|
||||
- **Testler arası state paylaşmayın**: Testler bağımsız olmalı
|
||||
- **Testlerde exception yakalamayın**: `pytest.raises` kullanın
|
||||
- **Print statement'ları kullanmayın**: Assertion'ları ve pytest çıktısını kullanın
|
||||
- **Çok kırılgan testler yazmayın**: Aşırı spesifik mock'lardan kaçının
|
||||
|
||||
## Yaygın Desenler
|
||||
|
||||
### API Endpoint'lerini Test Etme (FastAPI/Flask)
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = create_app(testing=True)
|
||||
return app.test_client()
|
||||
|
||||
def test_get_user(client):
|
||||
response = client.get("/api/users/1")
|
||||
assert response.status_code == 200
|
||||
assert response.json["id"] == 1
|
||||
|
||||
def test_create_user(client):
|
||||
response = client.post("/api/users", json={
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com"
|
||||
})
|
||||
assert response.status_code == 201
|
||||
assert response.json["name"] == "Alice"
|
||||
```
|
||||
|
||||
### Veritabanı Operasyonlarını Test Etme
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""Test veritabanı oturumu oluştur."""
|
||||
session = Session(bind=engine)
|
||||
session.begin_nested()
|
||||
yield session
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
def test_create_user(db_session):
|
||||
user = User(name="Alice", email="alice@example.com")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
retrieved = db_session.query(User).filter_by(name="Alice").first()
|
||||
assert retrieved.email == "alice@example.com"
|
||||
```
|
||||
|
||||
### Class Metodlarını Test Etme
|
||||
|
||||
```python
|
||||
class TestCalculator:
|
||||
@pytest.fixture
|
||||
def calculator(self):
|
||||
return Calculator()
|
||||
|
||||
def test_add(self, calculator):
|
||||
assert calculator.add(2, 3) == 5
|
||||
|
||||
def test_divide_by_zero(self, calculator):
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
calculator.divide(10, 0)
|
||||
```
|
||||
|
||||
## pytest Yapılandırması
|
||||
|
||||
### pytest.ini
|
||||
|
||||
```ini
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
--strict-markers
|
||||
--disable-warnings
|
||||
--cov=mypackage
|
||||
--cov-report=term-missing
|
||||
--cov-report=html
|
||||
markers =
|
||||
slow: marks tests as slow
|
||||
integration: marks tests as integration tests
|
||||
unit: marks tests as unit tests
|
||||
```
|
||||
|
||||
### pyproject.toml
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
addopts = [
|
||||
"--strict-markers",
|
||||
"--cov=mypackage",
|
||||
"--cov-report=term-missing",
|
||||
"--cov-report=html",
|
||||
]
|
||||
markers = [
|
||||
"slow: marks tests as slow",
|
||||
"integration: marks tests as integration tests",
|
||||
"unit: marks tests as unit tests",
|
||||
]
|
||||
```
|
||||
|
||||
## Testleri Çalıştırma
|
||||
|
||||
```bash
|
||||
# Tüm testleri çalıştır
|
||||
pytest
|
||||
|
||||
# Belirli dosyayı çalıştır
|
||||
pytest tests/test_utils.py
|
||||
|
||||
# Belirli testi çalıştır
|
||||
pytest tests/test_utils.py::test_function
|
||||
|
||||
# Verbose çıktı ile çalıştır
|
||||
pytest -v
|
||||
|
||||
# Coverage ile çalıştır
|
||||
pytest --cov=mypackage --cov-report=html
|
||||
|
||||
# Sadece hızlı testleri çalıştır
|
||||
pytest -m "not slow"
|
||||
|
||||
# İlk hataya kadar çalıştır
|
||||
pytest -x
|
||||
|
||||
# N hataya kadar çalıştır
|
||||
pytest --maxfail=3
|
||||
|
||||
# Son başarısız testleri çalıştır
|
||||
pytest --lf
|
||||
|
||||
# Pattern ile testleri çalıştır
|
||||
pytest -k "test_user"
|
||||
|
||||
# Hatada debugger ile çalıştır
|
||||
pytest --pdb
|
||||
```
|
||||
|
||||
## Hızlı Referans
|
||||
|
||||
| Desen | Kullanım |
|
||||
|-------|----------|
|
||||
| `pytest.raises()` | Beklenen exception'ları test et |
|
||||
| `@pytest.fixture()` | Yeniden kullanılabilir test fixture'ları oluştur |
|
||||
| `@pytest.mark.parametrize()` | Birden fazla input ile testleri çalıştır |
|
||||
| `@pytest.mark.slow` | Yavaş testleri işaretle |
|
||||
| `pytest -m "not slow"` | Yavaş testleri atla |
|
||||
| `@patch()` | Fonksiyonları ve class'ları mock'la |
|
||||
| `tmp_path` fixture | Otomatik geçici dizin |
|
||||
| `pytest --cov` | Coverage raporu oluştur |
|
||||
| `assert` | Basit ve okunabilir assertion'lar |
|
||||
|
||||
**Unutmayın**: Testler de koddur. Temiz, okunabilir ve bakımı kolay tutun. İyi testler hata yakalar; harika testler hataları önler.
|
||||
499
docs/tr/skills/rust-patterns/SKILL.md
Normal file
499
docs/tr/skills/rust-patterns/SKILL.md
Normal file
@@ -0,0 +1,499 @@
|
||||
---
|
||||
name: rust-patterns
|
||||
description: Idiomatic Rust patterns, ownership, error handling, traits, concurrency, and best practices for building safe, performant applications.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Rust Geliştirme Desenleri
|
||||
|
||||
Güvenli, performanslı ve bakım yapılabilir uygulamalar oluşturmak için idiomatic Rust desenleri ve en iyi uygulamalar.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Yeni Rust kodu yazma
|
||||
- Rust kodunu inceleme
|
||||
- Mevcut Rust kodunu refactor etme
|
||||
- Crate yapısı ve modül düzenini tasarlama
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
Bu skill altı ana alanda idiomatic Rust kurallarını zorlar: derleme zamanında veri yarışlarını önlemek için ownership ve borrowing, kütüphaneler için `thiserror` ve uygulamalar için `anyhow` ile `Result`/`?` hata yayılımı, yasadışı durumları temsil edilemez yapmak için enum'lar ve kapsamlı desen eşleştirme, sıfır maliyetli soyutlama için trait'ler ve generic'ler, `Arc<Mutex<T>>`, channel'lar ve async/await ile güvenli eşzamanlılık ve domain'e göre düzenlenmiş minimal `pub` yüzeyleri.
|
||||
|
||||
## Temel İlkeler
|
||||
|
||||
### 1. Ownership ve Borrowing
|
||||
|
||||
Rust'ın ownership sistemi derleme zamanında veri yarışlarını ve bellek hatalarını önler.
|
||||
|
||||
```rust
|
||||
// İyi: Ownership'e ihtiyacınız olmadığında referansları geçirin
|
||||
fn process(data: &[u8]) -> usize {
|
||||
data.len()
|
||||
}
|
||||
|
||||
// İyi: Saklamak veya tüketmek için ownership alın
|
||||
fn store(data: Vec<u8>) -> Record {
|
||||
Record { payload: data }
|
||||
}
|
||||
|
||||
// Kötü: Borrow checker'dan kaçınmak için gereksiz clone
|
||||
fn process_bad(data: &Vec<u8>) -> usize {
|
||||
let cloned = data.clone(); // İsraf — sadece borrow alın
|
||||
cloned.len()
|
||||
}
|
||||
```
|
||||
|
||||
### Esnek Ownership için `Cow` Kullanın
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn normalize(input: &str) -> Cow<'_, str> {
|
||||
if input.contains(' ') {
|
||||
Cow::Owned(input.replace(' ', "_"))
|
||||
} else {
|
||||
Cow::Borrowed(input) // Mutasyon gerekmediğinde sıfır maliyet
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hata İşleme
|
||||
|
||||
### `Result` ve `?` Kullanın — Production'da Asla `unwrap()` Kullanmayın
|
||||
|
||||
```rust
|
||||
// İyi: Hataları context ile yayın
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn load_config(path: &str) -> Result<Config> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read config from {path}"))?;
|
||||
let config: Config = toml::from_str(&content)
|
||||
.with_context(|| format!("failed to parse config from {path}"))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// Kötü: Hata durumunda panic
|
||||
fn load_config_bad(path: &str) -> Config {
|
||||
let content = std::fs::read_to_string(path).unwrap(); // Panic!
|
||||
toml::from_str(&content).unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
### Kütüphane Hataları için `thiserror`, Uygulama Hataları için `anyhow`
|
||||
|
||||
```rust
|
||||
// Kütüphane kodu: yapılandırılmış, tiplendirilmiş hatalar
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum StorageError {
|
||||
#[error("record not found: {id}")]
|
||||
NotFound { id: String },
|
||||
#[error("connection failed")]
|
||||
Connection(#[from] std::io::Error),
|
||||
#[error("invalid data: {0}")]
|
||||
InvalidData(String),
|
||||
}
|
||||
|
||||
// Uygulama kodu: esnek hata işleme
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let config = load_config("app.toml")?;
|
||||
if config.workers == 0 {
|
||||
bail!("worker count must be > 0");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### İç İçe Eşleştirme Yerine `Option` Combinator'ları
|
||||
|
||||
```rust
|
||||
// İyi: Combinator zinciri
|
||||
fn find_user_email(users: &[User], id: u64) -> Option<String> {
|
||||
users.iter()
|
||||
.find(|u| u.id == id)
|
||||
.map(|u| u.email.clone())
|
||||
}
|
||||
|
||||
// Kötü: Derinlemesine iç içe eşleştirme
|
||||
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
|
||||
match users.iter().find(|u| u.id == id) {
|
||||
Some(user) => match &user.email {
|
||||
email => Some(email.clone()),
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Enum'lar ve Desen Eşleştirme
|
||||
|
||||
### Durumları Enum'lar Olarak Modelleyin
|
||||
|
||||
```rust
|
||||
// İyi: İmkansız durumlar temsil edilemez
|
||||
enum ConnectionState {
|
||||
Disconnected,
|
||||
Connecting { attempt: u32 },
|
||||
Connected { session_id: String },
|
||||
Failed { reason: String, retries: u32 },
|
||||
}
|
||||
|
||||
fn handle(state: &ConnectionState) {
|
||||
match state {
|
||||
ConnectionState::Disconnected => connect(),
|
||||
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
|
||||
ConnectionState::Connecting { .. } => wait(),
|
||||
ConnectionState::Connected { session_id } => use_session(session_id),
|
||||
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
|
||||
ConnectionState::Failed { reason, .. } => log_failure(reason),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kapsamlı Eşleştirme — İş Mantığı için Catch-All Yok
|
||||
|
||||
```rust
|
||||
// İyi: Her varyantı açıkça işle
|
||||
match command {
|
||||
Command::Start => start_service(),
|
||||
Command::Stop => stop_service(),
|
||||
Command::Restart => restart_service(),
|
||||
// Yeni bir varyant eklemek burada işlemeyi zorlar
|
||||
}
|
||||
|
||||
// Kötü: Wildcard yeni varyantları gizler
|
||||
match command {
|
||||
Command::Start => start_service(),
|
||||
_ => {} // Stop, Restart ve gelecek varyantları sessizce yok sayar
|
||||
}
|
||||
```
|
||||
|
||||
## Trait'ler ve Generic'ler
|
||||
|
||||
### Generic Girişleri Kabul Et, Somut Türleri Döndür
|
||||
|
||||
```rust
|
||||
// İyi: Generic girdi, somut çıktı
|
||||
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// İyi: Birden fazla kısıtlama için trait bound'ları
|
||||
fn process<T: Display + Send + 'static>(item: T) -> String {
|
||||
format!("processed: {item}")
|
||||
}
|
||||
```
|
||||
|
||||
### Dinamik Dispatch için Trait Object'leri
|
||||
|
||||
```rust
|
||||
// Heterojen koleksiyonlara veya plugin sistemlerine ihtiyacınız olduğunda kullanın
|
||||
trait Handler: Send + Sync {
|
||||
fn handle(&self, request: &Request) -> Response;
|
||||
}
|
||||
|
||||
struct Router {
|
||||
handlers: Vec<Box<dyn Handler>>,
|
||||
}
|
||||
|
||||
// Performansa ihtiyacınız olduğunda generic'leri kullanın (monomorfizasyon)
|
||||
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
|
||||
handler.handle(request)
|
||||
}
|
||||
```
|
||||
|
||||
### Tip Güvenliği için Newtype Deseni
|
||||
|
||||
```rust
|
||||
// İyi: Farklı tipler argümanları karıştırmayı önler
|
||||
struct UserId(u64);
|
||||
struct OrderId(u64);
|
||||
|
||||
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
|
||||
// User ve order ID'lerini yanlışlıkla değiştiremezsiniz
|
||||
todo!()
|
||||
}
|
||||
|
||||
// Kötü: Argümanları değiştirmek kolay
|
||||
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
## Struct'lar ve Veri Modelleme
|
||||
|
||||
### Karmaşık Yapılandırma için Builder Deseni
|
||||
|
||||
```rust
|
||||
struct ServerConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
|
||||
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
|
||||
|
||||
impl ServerConfigBuilder {
|
||||
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
|
||||
fn build(self) -> ServerConfig {
|
||||
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
|
||||
}
|
||||
}
|
||||
|
||||
// Kullanım: ServerConfig::builder("localhost", 8080).max_connections(200).build()
|
||||
```
|
||||
|
||||
## Iterator'lar ve Closure'lar
|
||||
|
||||
### Manuel Döngüler Yerine Iterator Zincirlerini Tercih Edin
|
||||
|
||||
```rust
|
||||
// İyi: Deklaratif, lazy, birleştirilebilir
|
||||
let active_emails: Vec<String> = users.iter()
|
||||
.filter(|u| u.is_active)
|
||||
.map(|u| u.email.clone())
|
||||
.collect();
|
||||
|
||||
// Kötü: İmperatif biriktirme
|
||||
let mut active_emails = Vec::new();
|
||||
for user in &users {
|
||||
if user.is_active {
|
||||
active_emails.push(user.email.clone());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tip Annotation ile `collect()` Kullanın
|
||||
|
||||
```rust
|
||||
// Farklı tiplere collect et
|
||||
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
|
||||
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
|
||||
let combined: String = parts.iter().copied().collect();
|
||||
|
||||
// Result'ları collect et — ilk hatada kısa devre yapar
|
||||
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
|
||||
```
|
||||
|
||||
## Eşzamanlılık
|
||||
|
||||
### Paylaşılan Mutable State için `Arc<Mutex<T>>`
|
||||
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let counter = Arc::new(Mutex::new(0));
|
||||
let handles: Vec<_> = (0..10).map(|_| {
|
||||
let counter = Arc::clone(&counter);
|
||||
std::thread::spawn(move || {
|
||||
let mut num = counter.lock().expect("mutex poisoned");
|
||||
*num += 1;
|
||||
})
|
||||
}).collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().expect("worker thread panicked");
|
||||
}
|
||||
```
|
||||
|
||||
### Mesaj Geçişi için Channel'lar
|
||||
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel(16); // Backpressure ile bounded channel
|
||||
|
||||
for i in 0..5 {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
tx.send(format!("message {i}")).expect("receiver disconnected");
|
||||
});
|
||||
}
|
||||
drop(tx); // Sender'ı kapat böylece rx iterator sonlanır
|
||||
|
||||
for msg in rx {
|
||||
println!("{msg}");
|
||||
}
|
||||
```
|
||||
|
||||
### Tokio ile Async
|
||||
|
||||
```rust
|
||||
use tokio::time::Duration;
|
||||
|
||||
async fn fetch_with_timeout(url: &str) -> Result<String> {
|
||||
let response = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
reqwest::get(url),
|
||||
)
|
||||
.await
|
||||
.context("request timed out")?
|
||||
.context("request failed")?;
|
||||
|
||||
response.text().await.context("failed to read body")
|
||||
}
|
||||
|
||||
// Eşzamanlı görevler spawn et
|
||||
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
|
||||
let handles: Vec<_> = urls.into_iter()
|
||||
.map(|url| tokio::spawn(async move {
|
||||
fetch_with_timeout(&url).await
|
||||
}))
|
||||
.collect();
|
||||
|
||||
let mut results = Vec::with_capacity(handles.len());
|
||||
for handle in handles {
|
||||
results.push(handle.await.unwrap_or_else(|e| panic!("spawned task panicked: {e}")));
|
||||
}
|
||||
results
|
||||
}
|
||||
```
|
||||
|
||||
## Unsafe Kod
|
||||
|
||||
### Unsafe Ne Zaman Kabul Edilebilir
|
||||
|
||||
```rust
|
||||
// Kabul edilebilir: Belgelenmiş değişmezlerle FFI sınırı (Rust 2024+)
|
||||
/// # Safety
|
||||
/// `ptr` başlatılmış bir `Widget`'a geçerli, hizalı bir pointer olmalıdır.
|
||||
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
|
||||
// SAFETY: çağıran ptr'nin geçerli ve hizalı olduğunu garanti eder
|
||||
unsafe { &*ptr }
|
||||
}
|
||||
|
||||
// Kabul edilebilir: Doğruluk kanıtı ile performans-kritik yol
|
||||
// SAFETY: döngü sınırı nedeniyle index her zaman < len
|
||||
unsafe { slice.get_unchecked(index) }
|
||||
```
|
||||
|
||||
### Unsafe Ne Zaman Kabul EDİLEMEZ
|
||||
|
||||
```rust
|
||||
// Kötü: Borrow checker'ı atlamak için unsafe kullanma
|
||||
// Kötü: Kolaylık için unsafe kullanma
|
||||
// Kötü: Safety yorumu olmadan unsafe kullanma
|
||||
// Kötü: İlgisiz tipler arasında transmute etme
|
||||
```
|
||||
|
||||
## Modül Sistemi ve Crate Yapısı
|
||||
|
||||
### Tipe Göre Değil, Domain'e Göre Düzenle
|
||||
|
||||
```text
|
||||
my_app/
|
||||
├── src/
|
||||
│ ├── main.rs
|
||||
│ ├── lib.rs
|
||||
│ ├── auth/ # Domain modülü
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── token.rs
|
||||
│ │ └── middleware.rs
|
||||
│ ├── orders/ # Domain modülü
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── model.rs
|
||||
│ │ └── service.rs
|
||||
│ └── db/ # Altyapı
|
||||
│ ├── mod.rs
|
||||
│ └── pool.rs
|
||||
├── tests/ # Entegrasyon testleri
|
||||
├── benches/ # Benchmark'lar
|
||||
└── Cargo.toml
|
||||
```
|
||||
|
||||
### Görünürlük — Minimal Şekilde Açığa Çıkarın
|
||||
|
||||
```rust
|
||||
// İyi: Dahili paylaşım için pub(crate)
|
||||
pub(crate) fn validate_input(input: &str) -> bool {
|
||||
!input.is_empty()
|
||||
}
|
||||
|
||||
// İyi: lib.rs'den public API'yi yeniden export et
|
||||
pub mod auth;
|
||||
pub use auth::AuthMiddleware;
|
||||
|
||||
// Kötü: Her şeyi pub yapmak
|
||||
pub fn internal_helper() {} // pub(crate) veya private olmalı
|
||||
```
|
||||
|
||||
## Araç Entegrasyonu
|
||||
|
||||
### Temel Komutlar
|
||||
|
||||
```bash
|
||||
# Build ve kontrol
|
||||
cargo build
|
||||
cargo check # Codegen olmadan hızlı tip kontrolü
|
||||
cargo clippy # Lint'ler ve öneriler
|
||||
cargo fmt # Kodu formatla
|
||||
|
||||
# Test etme
|
||||
cargo test
|
||||
cargo test -- --nocapture # println çıktısını göster
|
||||
cargo test --lib # Sadece unit testler
|
||||
cargo test --test integration # Sadece entegrasyon testleri
|
||||
|
||||
# Bağımlılıklar
|
||||
cargo audit # Güvenlik denetimi
|
||||
cargo tree # Bağımlılık ağacı
|
||||
cargo update # Bağımlılıkları güncelle
|
||||
|
||||
# Performans
|
||||
cargo bench # Benchmark'ları çalıştır
|
||||
```
|
||||
|
||||
## Hızlı Referans: Rust Deyimleri
|
||||
|
||||
| Deyim | Açıklama |
|
||||
|-------|----------|
|
||||
| Clone etme, borrow al | Ownership gerekmedikçe clone yerine `&T` geçir |
|
||||
| Yasadışı durumları temsil edilemez yap | Sadece geçerli durumları modellemek için enum'ları kullan |
|
||||
| `unwrap()` yerine `?` | Hataları yay, kütüphane/production kodunda asla panic |
|
||||
| Validate etme, parse et | Sınırda yapılandırılmamış veriyi tiplendirilmiş struct'lara dönüştür |
|
||||
| Tip güvenliği için newtype | Argüman değişimlerini önlemek için primitive'leri newtype'lara sar |
|
||||
| Döngüler yerine iterator'ları tercih et | Deklaratif zincirler daha net ve genellikle daha hızlı |
|
||||
| Result'larda `#[must_use]` | Çağıranların dönüş değerlerini işlemesini garanti et |
|
||||
| Esnek ownership için `Cow` | Borrow yeterli olduğunda allocation'lardan kaçın |
|
||||
| Kapsamlı eşleştirme | İş-kritik enum'lar için wildcard `_` yok |
|
||||
| Minimal `pub` yüzeyi | Dahili API'ler için `pub(crate)` kullan |
|
||||
|
||||
## Kaçınılacak Anti-Desenler
|
||||
|
||||
```rust
|
||||
// Kötü: Production kodunda .unwrap()
|
||||
let value = map.get("key").unwrap();
|
||||
|
||||
// Kötü: Nedenini anlamadan borrow checker'ı tatmin etmek için .clone()
|
||||
let data = expensive_data.clone();
|
||||
process(&original, &data);
|
||||
|
||||
// Kötü: &str yeterken String kullanma
|
||||
fn greet(name: String) { /* &str olmalı */ }
|
||||
|
||||
// Kötü: Kütüphanelerde Box<dyn Error> (yerine thiserror kullanın)
|
||||
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
|
||||
|
||||
// Kötü: must_use uyarılarını yok sayma
|
||||
let _ = validate(input); // Bir Result'ı sessizce atma
|
||||
|
||||
// Kötü: Async context'te bloke etme
|
||||
async fn bad_async() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // Executor'ı bloke eder!
|
||||
// Kullanın: tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
```
|
||||
|
||||
**Unutmayın**: Derlenir ise muhtemelen doğrudur — ama sadece `unwrap()` kullanmaktan kaçınır, `unsafe`'i minimize eder ve tip sisteminin sizin için çalışmasına izin verirseniz.
|
||||
500
docs/tr/skills/rust-testing/SKILL.md
Normal file
500
docs/tr/skills/rust-testing/SKILL.md
Normal file
@@ -0,0 +1,500 @@
|
||||
---
|
||||
name: rust-testing
|
||||
description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Rust Test Desenleri
|
||||
|
||||
TDD metodolojisini takip ederek güvenilir, bakım yapılabilir testler yazmak için kapsamlı Rust test desenleri.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Yeni Rust fonksiyonları, metotları veya trait'leri yazma
|
||||
- Mevcut koda test kapsamı ekleme
|
||||
- Performans-kritik kod için benchmark'lar oluşturma
|
||||
- Girdi doğrulama için property-based testler uygulama
|
||||
- Rust projelerinde TDD iş akışını takip etme
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
1. **Hedef kodu tanımla** — Test edilecek fonksiyon, trait veya modülü bul
|
||||
2. **Bir test yaz** — `#[cfg(test)]` modülünde `#[test]` kullan, parametreli testler için rstest veya property-based testler için proptest
|
||||
3. **Bağımlılıkları mock'la** — Test altındaki birimi izole etmek için mockall kullan
|
||||
4. **Testleri çalıştır (RED)** — Testin beklenen hata ile başarısız olduğunu doğrula
|
||||
5. **Uygula (GREEN)** — Geçmek için minimal kod yaz
|
||||
6. **Refactor** — Testleri yeşil tutarken iyileştir
|
||||
7. **Kapsamı kontrol et** — cargo-llvm-cov kullan, 80%+ hedefle
|
||||
|
||||
## Rust için TDD İş Akışı
|
||||
|
||||
### RED-GREEN-REFACTOR Döngüsü
|
||||
|
||||
```
|
||||
RED → Önce başarısız bir test yaz
|
||||
GREEN → Testi geçmek için minimal kod yaz
|
||||
REFACTOR → Testleri yeşil tutarken kodu iyileştir
|
||||
REPEAT → Bir sonraki gereksinimle devam et
|
||||
```
|
||||
|
||||
### Rust'ta Adım-Adım TDD
|
||||
|
||||
```rust
|
||||
// RED: Önce testi yaz, yer tutucu olarak todo!() kullan
|
||||
pub fn add(a: i32, b: i32) -> i32 { todo!() }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_add() { assert_eq!(add(2, 3), 5); }
|
||||
}
|
||||
// cargo test → 'not yet implemented'da panic
|
||||
```
|
||||
|
||||
```rust
|
||||
// GREEN: todo!()'yu minimal implementasyonla değiştir
|
||||
pub fn add(a: i32, b: i32) -> i32 { a + b }
|
||||
// cargo test → GEÇTİ, sonra testleri yeşil tutarken REFACTOR
|
||||
```
|
||||
|
||||
## Unit Testler
|
||||
|
||||
### Modül Seviyesi Test Organizasyonu
|
||||
|
||||
```rust
|
||||
// src/user.rs
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> {
|
||||
let email = email.into();
|
||||
if !email.contains('@') {
|
||||
return Err(format!("invalid email: {email}"));
|
||||
}
|
||||
Ok(Self { name: name.into(), email })
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creates_user_with_valid_email() {
|
||||
let user = User::new("Alice", "alice@example.com").unwrap();
|
||||
assert_eq!(user.display_name(), "Alice");
|
||||
assert_eq!(user.email, "alice@example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_email() {
|
||||
let result = User::new("Bob", "not-an-email");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("invalid email"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Assertion Makroları
|
||||
|
||||
```rust
|
||||
assert_eq!(2 + 2, 4); // Eşitlik
|
||||
assert_ne!(2 + 2, 5); // Eşitsizlik
|
||||
assert!(vec![1, 2, 3].contains(&2)); // Boolean
|
||||
assert_eq!(value, 42, "expected 42 but got {value}"); // Özel mesaj
|
||||
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // Float karşılaştırma
|
||||
```
|
||||
|
||||
## Hata ve Panic Testi
|
||||
|
||||
### `Result` Dönüşlerini Test Etme
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn parse_returns_error_for_invalid_input() {
|
||||
let result = parse_config("}{invalid");
|
||||
assert!(result.is_err());
|
||||
|
||||
// Spesifik hata varyantını doğrula
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err, ConfigError::ParseError(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = parse_config(r#"{"port": 8080}"#)?;
|
||||
assert_eq!(config.port, 8080);
|
||||
Ok(()) // Herhangi bir ? Err döndürürse test başarısız olur
|
||||
}
|
||||
```
|
||||
|
||||
### Panic'leri Test Etme
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panics_on_empty_input() {
|
||||
process(&[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "index out of bounds")]
|
||||
fn panics_with_specific_message() {
|
||||
let v: Vec<i32> = vec![];
|
||||
let _ = v[0];
|
||||
}
|
||||
```
|
||||
|
||||
## Entegrasyon Testleri
|
||||
|
||||
### Dosya Yapısı
|
||||
|
||||
```text
|
||||
my_crate/
|
||||
├── src/
|
||||
│ └── lib.rs
|
||||
├── tests/ # Entegrasyon testleri
|
||||
│ ├── api_test.rs # Her dosya ayrı bir test binary'si
|
||||
│ ├── db_test.rs
|
||||
│ └── common/ # Paylaşılan test yardımcıları
|
||||
│ └── mod.rs
|
||||
```
|
||||
|
||||
### Entegrasyon Testleri Yazma
|
||||
|
||||
```rust
|
||||
// tests/api_test.rs
|
||||
use my_crate::{App, Config};
|
||||
|
||||
#[test]
|
||||
fn full_request_lifecycle() {
|
||||
let config = Config::test_default();
|
||||
let app = App::new(config);
|
||||
|
||||
let response = app.handle_request("/health");
|
||||
assert_eq!(response.status, 200);
|
||||
assert_eq!(response.body, "OK");
|
||||
}
|
||||
```
|
||||
|
||||
## Async Testler
|
||||
|
||||
### Tokio ile
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn fetches_data_successfully() {
|
||||
let client = TestClient::new().await;
|
||||
let result = client.get("/data").await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().items.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handles_timeout() {
|
||||
use std::time::Duration;
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
slow_operation(),
|
||||
).await;
|
||||
|
||||
assert!(result.is_err(), "should have timed out");
|
||||
}
|
||||
```
|
||||
|
||||
## Test Organizasyon Desenleri
|
||||
|
||||
### `rstest` ile Parametreli Testler
|
||||
|
||||
```rust
|
||||
use rstest::{rstest, fixture};
|
||||
|
||||
#[rstest]
|
||||
#[case("hello", 5)]
|
||||
#[case("", 0)]
|
||||
#[case("rust", 4)]
|
||||
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
|
||||
assert_eq!(input.len(), expected);
|
||||
}
|
||||
|
||||
// Fixture'lar
|
||||
#[fixture]
|
||||
fn test_db() -> TestDb {
|
||||
TestDb::new_in_memory()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_insert(test_db: TestDb) {
|
||||
test_db.insert("key", "value");
|
||||
assert_eq!(test_db.get("key"), Some("value".into()));
|
||||
}
|
||||
```
|
||||
|
||||
### Test Yardımcıları
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Mantıklı varsayılanlarla test kullanıcısı oluşturur.
|
||||
fn make_user(name: &str) -> User {
|
||||
User::new(name, &format!("{name}@test.com")).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_display() {
|
||||
let user = make_user("alice");
|
||||
assert_eq!(user.display_name(), "alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `proptest` ile Property-Based Testing
|
||||
|
||||
### Temel Property Testleri
|
||||
|
||||
```rust
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn encode_decode_roundtrip(input in ".*") {
|
||||
let encoded = encode(&input);
|
||||
let decoded = decode(&encoded).unwrap();
|
||||
assert_eq!(input, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
|
||||
let original_len = vec.len();
|
||||
vec.sort();
|
||||
assert_eq!(vec.len(), original_len);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
|
||||
vec.sort();
|
||||
for window in vec.windows(2) {
|
||||
assert!(window[0] <= window[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Özel Stratejiler
|
||||
|
||||
```rust
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn valid_email() -> impl Strategy<Value = String> {
|
||||
("[a-z]{1,10}", "[a-z]{1,5}")
|
||||
.prop_map(|(user, domain)| format!("{user}@{domain}.com"))
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn accepts_valid_emails(email in valid_email()) {
|
||||
assert!(User::new("Test", &email).is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `mockall` ile Mock'lama
|
||||
|
||||
### Trait-Tabanlı Mock'lama
|
||||
|
||||
```rust
|
||||
use mockall::{automock, predicate::eq};
|
||||
|
||||
#[automock]
|
||||
trait UserRepository {
|
||||
fn find_by_id(&self, id: u64) -> Option<User>;
|
||||
fn save(&self, user: &User) -> Result<(), StorageError>;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_returns_user_when_found() {
|
||||
let mut mock = MockUserRepository::new();
|
||||
mock.expect_find_by_id()
|
||||
.with(eq(42))
|
||||
.times(1)
|
||||
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
|
||||
|
||||
let service = UserService::new(Box::new(mock));
|
||||
let user = service.get_user(42).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_returns_none_when_not_found() {
|
||||
let mut mock = MockUserRepository::new();
|
||||
mock.expect_find_by_id()
|
||||
.returning(|_| None);
|
||||
|
||||
let service = UserService::new(Box::new(mock));
|
||||
assert!(service.get_user(99).is_none());
|
||||
}
|
||||
```
|
||||
|
||||
## Doc Testleri
|
||||
|
||||
### Çalıştırılabilir Dokümantasyon
|
||||
|
||||
```rust
|
||||
/// İki sayıyı toplar.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use my_crate::add;
|
||||
///
|
||||
/// assert_eq!(add(2, 3), 5);
|
||||
/// assert_eq!(add(-1, 1), 0);
|
||||
/// ```
|
||||
pub fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
|
||||
/// Bir config string'i parse eder.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Girdi geçerli TOML değilse `Err` döner.
|
||||
///
|
||||
/// ```no_run
|
||||
/// use my_crate::parse_config;
|
||||
///
|
||||
/// let config = parse_config(r#"port = 8080"#).unwrap();
|
||||
/// assert_eq!(config.port, 8080);
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// use my_crate::parse_config;
|
||||
///
|
||||
/// assert!(parse_config("}{invalid").is_err());
|
||||
/// ```
|
||||
pub fn parse_config(input: &str) -> Result<Config, ParseError> {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
## Criterion ile Benchmark'lama
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
```
|
||||
|
||||
```rust
|
||||
// benches/benchmark.rs
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn fibonacci(n: u64) -> u64 {
|
||||
match n {
|
||||
0 | 1 => n,
|
||||
_ => fibonacci(n - 1) + fibonacci(n - 2),
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_fibonacci(c: &mut Criterion) {
|
||||
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_fibonacci);
|
||||
criterion_main!(benches);
|
||||
```
|
||||
|
||||
## Test Kapsamı
|
||||
|
||||
### Kapsamı Çalıştırma
|
||||
|
||||
```bash
|
||||
# Kurulum: cargo install cargo-llvm-cov (veya CI'da taiki-e/install-action kullan)
|
||||
cargo llvm-cov # Özet
|
||||
cargo llvm-cov --html # HTML raporu
|
||||
cargo llvm-cov --lcov > lcov.info # CI için LCOV formatı
|
||||
cargo llvm-cov --fail-under-lines 80 # Eşiğin altındaysa başarısız yap
|
||||
```
|
||||
|
||||
### Kapsam Hedefleri
|
||||
|
||||
| Kod Tipi | Hedef |
|
||||
|----------|-------|
|
||||
| Kritik iş mantığı | 100% |
|
||||
| Public API | 90%+ |
|
||||
| Genel kod | 80%+ |
|
||||
| Oluşturulmuş / FFI binding'leri | Hariç tut |
|
||||
|
||||
## Test Komutları
|
||||
|
||||
```bash
|
||||
cargo test # Tüm testleri çalıştır
|
||||
cargo test -- --nocapture # println çıktısını göster
|
||||
cargo test test_name # Desene uyan testleri çalıştır
|
||||
cargo test --lib # Sadece unit testler
|
||||
cargo test --test api_test # Sadece entegrasyon testleri
|
||||
cargo test --doc # Sadece doc testleri
|
||||
cargo test --no-fail-fast # İlk başarısızlıkta durma
|
||||
cargo test -- --ignored # Yok sayılan testleri çalıştır
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
**YAPIN:**
|
||||
- ÖNCE testleri yazın (TDD)
|
||||
- Unit testler için `#[cfg(test)]` modülleri kullanın
|
||||
- Implementasyon değil, davranışı test edin
|
||||
- Senaryoyu açıklayan açıklayıcı test isimleri kullanın
|
||||
- Daha iyi hata mesajları için `assert!` yerine `assert_eq!` tercih edin
|
||||
- Daha temiz hata çıktısı için `Result` döndüren testlerde `?` kullanın
|
||||
- Testleri bağımsız tutun — paylaşılan mutable state yok
|
||||
|
||||
**YAPMAYIN:**
|
||||
- `Result::is_err()` test edebiliyorsanız `#[should_panic]` kullanmayın
|
||||
- Her şeyi mock'lamayın — mümkün olduğunda entegrasyon testlerini tercih edin
|
||||
- Kararsız testleri yok saymayın — düzeltin veya karantinaya alın
|
||||
- Testlerde `sleep()` kullanmayın — channel'lar, barrier'lar veya `tokio::time::pause()` kullanın
|
||||
- Hata yolu testini atlamayın
|
||||
|
||||
## CI Entegrasyonu
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
|
||||
- name: Coverage
|
||||
run: cargo llvm-cov --fail-under-lines 80
|
||||
```
|
||||
|
||||
**Unutmayın**: Testler dokümantasyondur. Kodunuzun nasıl kullanılması gerektiğini gösterirler. Onları net yazın ve güncel tutun.
|
||||
495
docs/tr/skills/security-review/SKILL.md
Normal file
495
docs/tr/skills/security-review/SKILL.md
Normal file
@@ -0,0 +1,495 @@
|
||||
---
|
||||
name: security-review
|
||||
description: Kimlik doğrulama eklerken, kullanıcı girdisi işlerken, secret'larla çalışırken, API endpoint'leri oluştururken veya ödeme/hassas özellikler uygularken bu skill'i kullanın. Kapsamlı güvenlik kontrol listesi ve kalıplar sağlar.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Güvenlik İnceleme Skill'i
|
||||
|
||||
Bu skill tüm kodun güvenlik en iyi uygulamalarını takip etmesini sağlar ve potansiyel güvenlik açıklarını tanımlar.
|
||||
|
||||
## Ne Zaman Aktifleştirmelisiniz
|
||||
|
||||
- Kimlik doğrulama veya yetkilendirme uygularken
|
||||
- Kullanıcı girdisi veya dosya yüklemeleri işlerken
|
||||
- Yeni API endpoint'leri oluştururken
|
||||
- Secret'lar veya kimlik bilgileriyle çalışırken
|
||||
- Ödeme özellikleri uygularken
|
||||
- Hassas veri saklarken veya iletirken
|
||||
- Üçüncü taraf API'leri entegre ederken
|
||||
|
||||
## Güvenlik Kontrol Listesi
|
||||
|
||||
### 1. Secret Yönetimi
|
||||
|
||||
#### ❌ ASLA Bunu Yapmayın
|
||||
```typescript
|
||||
const apiKey = "sk-proj-xxxxx" // Hardcoded secret
|
||||
const dbPassword = "password123" // Kaynak kodda
|
||||
```
|
||||
|
||||
#### ✅ HER ZAMAN Bunu Yapın
|
||||
```typescript
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
const dbUrl = process.env.DATABASE_URL
|
||||
|
||||
// Secret'ların var olduğunu doğrula
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY not configured')
|
||||
}
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Hardcoded API key, token veya şifre yok
|
||||
- [ ] Tüm secret'lar environment variable'larda
|
||||
- [ ] `.env.local` .gitignore'da
|
||||
- [ ] Git history'de secret yok
|
||||
- [ ] Production secret'ları hosting platformunda (Vercel, Railway)
|
||||
|
||||
### 2. Input Doğrulama
|
||||
|
||||
#### Her Zaman Kullanıcı Girdisini Doğrulayın
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// Doğrulama şeması tanımla
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
age: z.number().int().min(0).max(150)
|
||||
})
|
||||
|
||||
// İşlemeden önce doğrula
|
||||
export async function createUser(input: unknown) {
|
||||
try {
|
||||
const validated = CreateUserSchema.parse(input)
|
||||
return await db.users.create(validated)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return { success: false, errors: error.errors }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Dosya Yükleme Doğrulama
|
||||
```typescript
|
||||
function validateFileUpload(file: File) {
|
||||
// Boyut kontrolü (5MB max)
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Dosya çok büyük (max 5MB)')
|
||||
}
|
||||
|
||||
// Tip kontrolü
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Geçersiz dosya tipi')
|
||||
}
|
||||
|
||||
// Uzantı kontrolü
|
||||
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
|
||||
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
|
||||
if (!extension || !allowedExtensions.includes(extension)) {
|
||||
throw new Error('Geçersiz dosya uzantısı')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Tüm kullanıcı girdileri şema ile doğrulanmış
|
||||
- [ ] Dosya yüklemeleri kısıtlanmış (boyut, tip, uzantı)
|
||||
- [ ] Kullanıcı girdisi doğrudan sorgularda kullanılmıyor
|
||||
- [ ] Whitelist doğrulama (blacklist değil)
|
||||
- [ ] Hata mesajları hassas bilgi sızdırmıyor
|
||||
|
||||
### 3. SQL Injection Önleme
|
||||
|
||||
#### ❌ ASLA SQL Concatenation Yapmayın
|
||||
```typescript
|
||||
// TEHLİKELİ - SQL Injection açığı
|
||||
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
|
||||
await db.query(query)
|
||||
```
|
||||
|
||||
#### ✅ HER ZAMAN Parametreli Sorgular Kullanın
|
||||
```typescript
|
||||
// Güvenli - parametreli sorgu
|
||||
const { data } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('email', userEmail)
|
||||
|
||||
// Veya raw SQL ile
|
||||
await db.query(
|
||||
'SELECT * FROM users WHERE email = $1',
|
||||
[userEmail]
|
||||
)
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Tüm veritabanı sorguları parametreli
|
||||
- [ ] SQL'de string concatenation yok
|
||||
- [ ] ORM/query builder doğru kullanılıyor
|
||||
- [ ] Supabase sorguları düzgün sanitize edilmiş
|
||||
|
||||
### 4. Kimlik Doğrulama ve Yetkilendirme
|
||||
|
||||
#### JWT Token İşleme
|
||||
```typescript
|
||||
// ❌ YANLIŞ: localStorage (XSS'e karşı savunmasız)
|
||||
localStorage.setItem('token', token)
|
||||
|
||||
// ✅ DOĞRU: httpOnly cookies
|
||||
res.setHeader('Set-Cookie',
|
||||
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
|
||||
```
|
||||
|
||||
#### Yetkilendirme Kontrolleri
|
||||
```typescript
|
||||
export async function deleteUser(userId: string, requesterId: string) {
|
||||
// HER ZAMAN önce yetkilendirmeyi doğrula
|
||||
const requester = await db.users.findUnique({
|
||||
where: { id: requesterId }
|
||||
})
|
||||
|
||||
if (requester.role !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Silme işlemine devam et
|
||||
await db.users.delete({ where: { id: userId } })
|
||||
}
|
||||
```
|
||||
|
||||
#### Row Level Security (Supabase)
|
||||
```sql
|
||||
-- Tüm tablolarda RLS'yi aktifleştir
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Kullanıcılar sadece kendi verilerini görebilir
|
||||
CREATE POLICY "Users view own data"
|
||||
ON users FOR SELECT
|
||||
USING (auth.uid() = id);
|
||||
|
||||
-- Kullanıcılar sadece kendi verilerini güncelleyebilir
|
||||
CREATE POLICY "Users update own data"
|
||||
ON users FOR UPDATE
|
||||
USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Token'lar httpOnly cookie'lerde (localStorage'da değil)
|
||||
- [ ] Hassas operasyonlardan önce yetkilendirme kontrolleri
|
||||
- [ ] Supabase'de Row Level Security aktif
|
||||
- [ ] Rol tabanlı erişim kontrolü uygulanmış
|
||||
- [ ] Session yönetimi güvenli
|
||||
|
||||
### 5. XSS Önleme
|
||||
|
||||
#### HTML'i Sanitize Et
|
||||
```typescript
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
|
||||
// HER ZAMAN kullanıcı tarafından sağlanan HTML'i sanitize et
|
||||
function renderUserContent(html: string) {
|
||||
const clean = DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
|
||||
ALLOWED_ATTR: []
|
||||
})
|
||||
return <div dangerouslySetInnerHTML={{ __html: clean }} />
|
||||
}
|
||||
```
|
||||
|
||||
#### Content Security Policy
|
||||
```typescript
|
||||
// next.config.js
|
||||
const securityHeaders = [
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self';
|
||||
connect-src 'self' https://api.example.com;
|
||||
`.replace(/\s{2,}/g, ' ').trim()
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Kullanıcı tarafından sağlanan HTML sanitize edilmiş
|
||||
- [ ] CSP başlıkları yapılandırılmış
|
||||
- [ ] Doğrulanmamış dinamik içerik render'ı yok
|
||||
- [ ] React'in yerleşik XSS koruması kullanılıyor
|
||||
|
||||
### 6. CSRF Koruması
|
||||
|
||||
#### CSRF Token'ları
|
||||
```typescript
|
||||
import { csrf } from '@/lib/csrf'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const token = request.headers.get('X-CSRF-Token')
|
||||
|
||||
if (!csrf.verify(token)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid CSRF token' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// İsteği işle
|
||||
}
|
||||
```
|
||||
|
||||
#### SameSite Cookie'ler
|
||||
```typescript
|
||||
res.setHeader('Set-Cookie',
|
||||
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] State değiştiren operasyonlarda CSRF token'ları
|
||||
- [ ] Tüm cookie'lerde SameSite=Strict
|
||||
- [ ] Double-submit cookie pattern uygulanmış
|
||||
|
||||
### 7. Rate Limiting
|
||||
|
||||
#### API Rate Limiting
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 dakika
|
||||
max: 100, // Pencere başına 100 istek
|
||||
message: 'Çok fazla istek'
|
||||
})
|
||||
|
||||
// Route'lara uygula
|
||||
app.use('/api/', limiter)
|
||||
```
|
||||
|
||||
#### Pahalı Operasyonlar
|
||||
```typescript
|
||||
// Aramalar için agresif rate limiting
|
||||
const searchLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 dakika
|
||||
max: 10, // Dakikada 10 istek
|
||||
message: 'Çok fazla arama isteği'
|
||||
})
|
||||
|
||||
app.use('/api/search', searchLimiter)
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Tüm API endpoint'lerinde rate limiting
|
||||
- [ ] Pahalı operasyonlarda daha sıkı limitler
|
||||
- [ ] IP tabanlı rate limiting
|
||||
- [ ] Kullanıcı tabanlı rate limiting (authenticated)
|
||||
|
||||
### 8. Hassas Veri İfşası
|
||||
|
||||
#### Loglama
|
||||
```typescript
|
||||
// ❌ YANLIŞ: Hassas veri loglama
|
||||
console.log('User login:', { email, password })
|
||||
console.log('Payment:', { cardNumber, cvv })
|
||||
|
||||
// ✅ DOĞRU: Hassas veriyi gizle
|
||||
console.log('User login:', { email, userId })
|
||||
console.log('Payment:', { last4: card.last4, userId })
|
||||
```
|
||||
|
||||
#### Hata Mesajları
|
||||
```typescript
|
||||
// ❌ YANLIŞ: İç detayları açığa çıkarma
|
||||
catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message, stack: error.stack },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ DOĞRU: Genel hata mesajları
|
||||
catch (error) {
|
||||
console.error('Internal error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Bir hata oluştu. Lütfen tekrar deneyin.' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Loglarda şifre, token veya secret yok
|
||||
- [ ] Kullanıcılar için genel hata mesajları
|
||||
- [ ] Detaylı hatalar sadece sunucu loglarında
|
||||
- [ ] Kullanıcılara stack trace gösterilmiyor
|
||||
|
||||
### 9. Blockchain Güvenliği (Solana)
|
||||
|
||||
#### Wallet Doğrulama
|
||||
```typescript
|
||||
import { verify } from '@solana/web3.js'
|
||||
|
||||
async function verifyWalletOwnership(
|
||||
publicKey: string,
|
||||
signature: string,
|
||||
message: string
|
||||
) {
|
||||
try {
|
||||
const isValid = verify(
|
||||
Buffer.from(message),
|
||||
Buffer.from(signature, 'base64'),
|
||||
Buffer.from(publicKey, 'base64')
|
||||
)
|
||||
return isValid
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Transaction Doğrulama
|
||||
```typescript
|
||||
async function verifyTransaction(transaction: Transaction) {
|
||||
// Alıcıyı doğrula
|
||||
if (transaction.to !== expectedRecipient) {
|
||||
throw new Error('Geçersiz alıcı')
|
||||
}
|
||||
|
||||
// Miktarı doğrula
|
||||
if (transaction.amount > maxAmount) {
|
||||
throw new Error('Miktar limiti aşıyor')
|
||||
}
|
||||
|
||||
// Kullanıcının yeterli bakiyesi olduğunu doğrula
|
||||
const balance = await getBalance(transaction.from)
|
||||
if (balance < transaction.amount) {
|
||||
throw new Error('Yetersiz bakiye')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Wallet imzaları doğrulanmış
|
||||
- [ ] Transaction detayları validate edilmiş
|
||||
- [ ] Transaction'lardan önce bakiye kontrolleri
|
||||
- [ ] Kör transaction imzalama yok
|
||||
|
||||
### 10. Bağımlılık Güvenliği
|
||||
|
||||
#### Düzenli Güncellemeler
|
||||
```bash
|
||||
# Güvenlik açıklarını kontrol et
|
||||
npm audit
|
||||
|
||||
# Otomatik düzeltilebilir sorunları düzelt
|
||||
npm audit fix
|
||||
|
||||
# Bağımlılıkları güncelle
|
||||
npm update
|
||||
|
||||
# Eski paketleri kontrol et
|
||||
npm outdated
|
||||
```
|
||||
|
||||
#### Lock Dosyaları
|
||||
```bash
|
||||
# HER ZAMAN lock dosyalarını commit et
|
||||
git add package-lock.json
|
||||
|
||||
# CI/CD'de tekrarlanabilir build'ler için kullan
|
||||
npm ci # npm install yerine
|
||||
```
|
||||
|
||||
#### Doğrulama Adımları
|
||||
- [ ] Bağımlılıklar güncel
|
||||
- [ ] Bilinen güvenlik açığı yok (npm audit clean)
|
||||
- [ ] Lock dosyaları commit edilmiş
|
||||
- [ ] GitHub'da Dependabot aktif
|
||||
- [ ] Düzenli güvenlik güncellemeleri
|
||||
|
||||
## Güvenlik Testi
|
||||
|
||||
### Otomatik Güvenlik Testleri
|
||||
```typescript
|
||||
// Kimlik doğrulama testi
|
||||
test('kimlik doğrulama gerektirir', async () => {
|
||||
const response = await fetch('/api/protected')
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
// Yetkilendirme testi
|
||||
test('admin rolü gerektirir', async () => {
|
||||
const response = await fetch('/api/admin', {
|
||||
headers: { Authorization: `Bearer ${userToken}` }
|
||||
})
|
||||
expect(response.status).toBe(403)
|
||||
})
|
||||
|
||||
// Input doğrulama testi
|
||||
test('geçersiz input'u reddeder', async () => {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: 'not-an-email' })
|
||||
})
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
// Rate limiting testi
|
||||
test('rate limit'leri zorlar', async () => {
|
||||
const requests = Array(101).fill(null).map(() =>
|
||||
fetch('/api/endpoint')
|
||||
)
|
||||
|
||||
const responses = await Promise.all(requests)
|
||||
const tooManyRequests = responses.filter(r => r.status === 429)
|
||||
|
||||
expect(tooManyRequests.length).toBeGreaterThan(0)
|
||||
})
|
||||
```
|
||||
|
||||
## Deployment Öncesi Güvenlik Kontrol Listesi
|
||||
|
||||
HERHANGİ bir production deployment'ından önce:
|
||||
|
||||
- [ ] **Secret'lar**: Hardcoded secret yok, hepsi env var'larda
|
||||
- [ ] **Input Doğrulama**: Tüm kullanıcı girdileri validate edilmiş
|
||||
- [ ] **SQL Injection**: Tüm sorgular parametreli
|
||||
- [ ] **XSS**: Kullanıcı içeriği sanitize edilmiş
|
||||
- [ ] **CSRF**: Koruma aktif
|
||||
- [ ] **Kimlik Doğrulama**: Doğru token işleme
|
||||
- [ ] **Yetkilendirme**: Rol kontrolleri yerinde
|
||||
- [ ] **Rate Limiting**: Tüm endpoint'lerde aktif
|
||||
- [ ] **HTTPS**: Production'da zorunlu
|
||||
- [ ] **Güvenlik Başlıkları**: CSP, X-Frame-Options yapılandırılmış
|
||||
- [ ] **Hata İşleme**: Hatalarda hassas veri yok
|
||||
- [ ] **Loglama**: Hassas veri loglanmıyor
|
||||
- [ ] **Bağımlılıklar**: Güncel, güvenlik açığı yok
|
||||
- [ ] **Row Level Security**: Supabase'de aktif
|
||||
- [ ] **CORS**: Düzgün yapılandırılmış
|
||||
- [ ] **Dosya Yüklemeleri**: Validate edilmiş (boyut, tip)
|
||||
- [ ] **Wallet İmzaları**: Doğrulanmış (blockchain varsa)
|
||||
|
||||
## Kaynaklar
|
||||
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [Next.js Security](https://nextjs.org/docs/security)
|
||||
- [Supabase Security](https://supabase.com/docs/guides/auth)
|
||||
- [Web Security Academy](https://portswigger.net/web-security)
|
||||
|
||||
---
|
||||
|
||||
**Unutmayın**: Güvenlik opsiyonel değildir. Bir güvenlik açığı tüm platformu tehlikeye atabilir. Şüphe duyduğunuzda ihtiyatlı olun.
|
||||
312
docs/tr/skills/springboot-patterns/SKILL.md
Normal file
312
docs/tr/skills/springboot-patterns/SKILL.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
name: springboot-patterns
|
||||
description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Spring Boot Geliştirme Desenleri
|
||||
|
||||
Ölçeklenebilir, üretim seviyesi servisler için Spring Boot mimari ve API desenleri.
|
||||
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
- Spring MVC veya WebFlux ile REST API'leri oluşturma
|
||||
- Controller → service → repository katmanlarını yapılandırma
|
||||
- Spring Data JPA, caching veya async processing'i yapılandırma
|
||||
- Validation, exception handling veya sayfalama ekleme
|
||||
- Dev/staging/production ortamları için profiller kurma
|
||||
- Spring Events veya Kafka ile event-driven desenler uygulama
|
||||
|
||||
## REST API Yapısı
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/markets")
|
||||
@Validated
|
||||
class MarketController {
|
||||
private final MarketService marketService;
|
||||
|
||||
MarketController(MarketService marketService) {
|
||||
this.marketService = marketService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
ResponseEntity<Page<MarketResponse>> list(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
Page<Market> markets = marketService.list(PageRequest.of(page, size));
|
||||
return ResponseEntity.ok(markets.map(MarketResponse::from));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {
|
||||
Market market = marketService.create(request);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse::from(market));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Deseni (Spring Data JPA)
|
||||
|
||||
```java
|
||||
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
|
||||
@Query("select m from MarketEntity m where m.status = :status order by m.volume desc")
|
||||
List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);
|
||||
}
|
||||
```
|
||||
|
||||
## Transaction'lı Service Katmanı
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class MarketService {
|
||||
private final MarketRepository repo;
|
||||
|
||||
public MarketService(MarketRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Market create(CreateMarketRequest request) {
|
||||
MarketEntity entity = MarketEntity.from(request);
|
||||
MarketEntity saved = repo.save(entity);
|
||||
return Market.from(saved);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DTO'lar ve Validation
|
||||
|
||||
```java
|
||||
public record CreateMarketRequest(
|
||||
@NotBlank @Size(max = 200) String name,
|
||||
@NotBlank @Size(max = 2000) String description,
|
||||
@NotNull @FutureOrPresent Instant endDate,
|
||||
@NotEmpty List<@NotBlank String> categories) {}
|
||||
|
||||
public record MarketResponse(Long id, String name, MarketStatus status) {
|
||||
static MarketResponse from(Market market) {
|
||||
return new MarketResponse(market.id(), market.name(), market.status());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Handling
|
||||
|
||||
```java
|
||||
@ControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
|
||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
return ResponseEntity.badRequest().body(ApiError.validation(message));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
ResponseEntity<ApiError> handleAccessDenied() {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
ResponseEntity<ApiError> handleGeneric(Exception ex) {
|
||||
// Beklenmeyen hataları stack trace'ler ile loglayın
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiError.of("Internal server error"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
Bir configuration sınıfında `@EnableCaching` gerektirir.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class MarketCacheService {
|
||||
private final MarketRepository repo;
|
||||
|
||||
public MarketCacheService(MarketRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@Cacheable(value = "market", key = "#id")
|
||||
public Market getById(Long id) {
|
||||
return repo.findById(id)
|
||||
.map(Market::from)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
|
||||
}
|
||||
|
||||
@CacheEvict(value = "market", key = "#id")
|
||||
public void evict(Long id) {}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Processing
|
||||
|
||||
Bir configuration sınıfında `@EnableAsync` gerektirir.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class NotificationService {
|
||||
@Async
|
||||
public CompletableFuture<Void> sendAsync(Notification notification) {
|
||||
// email/SMS gönder
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Loglama (SLF4J)
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class ReportService {
|
||||
private static final Logger log = LoggerFactory.getLogger(ReportService.class);
|
||||
|
||||
public Report generate(Long marketId) {
|
||||
log.info("generate_report marketId={}", marketId);
|
||||
try {
|
||||
// mantık
|
||||
} catch (Exception ex) {
|
||||
log.error("generate_report_failed marketId={}", marketId, ex);
|
||||
throw ex;
|
||||
}
|
||||
return new Report();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Middleware / Filter'lar
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class RequestLoggingFilter extends OncePerRequestFilter {
|
||||
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
log.info("req method={} uri={} status={} durationMs={}",
|
||||
request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sayfalama ve Sıralama
|
||||
|
||||
```java
|
||||
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
|
||||
Page<Market> results = marketService.list(page);
|
||||
```
|
||||
|
||||
## Hata-Dayanıklı Harici Çağrılar
|
||||
|
||||
```java
|
||||
public <T> T withRetry(Supplier<T> supplier, int maxRetries) {
|
||||
int attempts = 0;
|
||||
while (true) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception ex) {
|
||||
attempts++;
|
||||
if (attempts >= maxRetries) {
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
Thread.sleep((long) Math.pow(2, attempts) * 100L);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting (Filter + Bucket4j)
|
||||
|
||||
**Güvenlik Notu**: `X-Forwarded-For` başlığı varsayılan olarak güvenilmezdir çünkü istemciler onu taklit edebilir.
|
||||
Forwarded başlıkları sadece şu durumlarda kullanın:
|
||||
1. Uygulamanız güvenilir bir reverse proxy'nin arkasında (nginx, AWS ALB, vb.)
|
||||
2. `ForwardedHeaderFilter`'ı bean olarak kaydetmişsiniz
|
||||
3. application properties'de `server.forward-headers-strategy=NATIVE` veya `FRAMEWORK` yapılandırmışsınız
|
||||
4. Proxy'niz `X-Forwarded-For` başlığını üzerine yazmak için yapılandırılmış (eklememek için değil)
|
||||
|
||||
`ForwardedHeaderFilter` düzgün yapılandırıldığında, `request.getRemoteAddr()` otomatik olarak
|
||||
forwarded başlıklardan doğru istemci IP'sini döndürür. Bu yapılandırma olmadan, `request.getRemoteAddr()` doğrudan kullanın—anlık bağlantı IP'sini döndürür, bu güvenilir tek değerdir.
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class RateLimitFilter extends OncePerRequestFilter {
|
||||
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
||||
|
||||
/*
|
||||
* GÜVENLİK: Bu filtre rate limiting için istemcileri tanımlamak üzere request.getRemoteAddr() kullanır.
|
||||
*
|
||||
* Uygulamanız bir reverse proxy'nin (nginx, AWS ALB, vb.) arkasındaysa, doğru istemci IP tespiti için
|
||||
* Spring'i forwarded başlıkları düzgün işleyecek şekilde yapılandırmalısınız:
|
||||
*
|
||||
* 1. application.properties/yaml'da server.forward-headers-strategy=NATIVE (cloud platformlar için)
|
||||
* veya FRAMEWORK ayarlayın
|
||||
* 2. FRAMEWORK stratejisi kullanıyorsanız, ForwardedHeaderFilter'ı kaydedin:
|
||||
*
|
||||
* @Bean
|
||||
* ForwardedHeaderFilter forwardedHeaderFilter() {
|
||||
* return new ForwardedHeaderFilter();
|
||||
* }
|
||||
*
|
||||
* 3. Proxy'nizin sahteciliği önlemek için X-Forwarded-For başlığını üzerine yazdığından emin olun (eklemediğinden)
|
||||
* 4. Container'ınız için server.tomcat.remoteip.trusted-proxies veya eşdeğerini yapılandırın
|
||||
*
|
||||
* Bu yapılandırma olmadan, request.getRemoteAddr() istemci IP'si değil proxy IP'si döndürür.
|
||||
* X-Forwarded-For'u doğrudan okumayın—güvenilir proxy işleme olmadan kolayca taklit edilebilir.
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
// ForwardedHeaderFilter yapılandırıldığında doğru istemci IP'sini döndüren
|
||||
// veya aksi halde doğrudan bağlantı IP'sini döndüren getRemoteAddr() kullanın. X-Forwarded-For
|
||||
// başlıklarına doğrudan güvenmeyin, düzgun proxy yapılandırması olmadan.
|
||||
String clientIp = request.getRemoteAddr();
|
||||
|
||||
Bucket bucket = buckets.computeIfAbsent(clientIp,
|
||||
k -> Bucket.builder()
|
||||
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
|
||||
.build());
|
||||
|
||||
if (bucket.tryConsume(1)) {
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Arka Plan Job'ları
|
||||
|
||||
Spring'in `@Scheduled`'ını kullanın veya kuyruklar ile entegre olun (örn. Kafka, SQS, RabbitMQ). Handler'ları idempotent ve gözlemlenebilir tutun.
|
||||
|
||||
## Gözlemlenebilirlik
|
||||
|
||||
- Logback encoder ile yapılandırılmış loglama (JSON)
|
||||
- Metrikler: Micrometer + Prometheus/OTel
|
||||
- Tracing: OpenTelemetry veya Brave backend ile Micrometer Tracing
|
||||
|
||||
## Production Varsayılanları
|
||||
|
||||
- Constructor injection'ı tercih edin, field injection'dan kaçının
|
||||
- RFC 7807 hataları için `spring.mvc.problemdetails.enabled=true` etkinleştirin (Spring Boot 3+)
|
||||
- İş yükü için HikariCP pool boyutlarını yapılandırın, timeout'ları ayarlayın
|
||||
- Sorgular için `@Transactional(readOnly = true)` kullanın
|
||||
- `@NonNull` ve uygun yerlerde `Optional` ile null-safety zorlayın
|
||||
|
||||
**Unutmayın**: Controller'ları ince, servisleri odaklı, repository'leri basit ve hataları merkezi olarak işlenmiş tutun. Bakım yapılabilirlik ve test edilebilirlik için optimize edin.
|
||||
272
docs/tr/skills/springboot-security/SKILL.md
Normal file
272
docs/tr/skills/springboot-security/SKILL.md
Normal file
@@ -0,0 +1,272 @@
|
||||
---
|
||||
name: springboot-security
|
||||
description: Spring Security best practices for authn/authz, validation, CSRF, secrets, headers, rate limiting, and dependency security in Java Spring Boot services.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Spring Boot Güvenlik İncelemesi
|
||||
|
||||
Auth ekleme, girişi işleme, endpoint oluşturma veya gizli bilgilerle uğraşırken kullanın.
|
||||
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
- Kimlik doğrulama ekleme (JWT, OAuth2, session-based)
|
||||
- Yetkilendirme uygulama (@PreAuthorize, role-based erişim)
|
||||
- Kullanıcı girişini doğrulama (Bean Validation, custom validator'lar)
|
||||
- CORS, CSRF veya güvenlik başlıklarını yapılandırma
|
||||
- Gizli bilgileri yönetme (Vault, ortam değişkenleri)
|
||||
- Rate limiting veya brute-force koruması ekleme
|
||||
- Bağımlılıkları CVE için tarama
|
||||
|
||||
## Kimlik Doğrulama
|
||||
|
||||
- İptal listesi ile stateless JWT veya opaque token'ları tercih edin
|
||||
- Session'lar için `httpOnly`, `Secure`, `SameSite=Strict` cookie'leri kullanın
|
||||
- Token'ları `OncePerRequestFilter` veya resource server ile doğrulayın
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class JwtAuthFilter extends OncePerRequestFilter {
|
||||
private final JwtService jwtService;
|
||||
|
||||
public JwtAuthFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain chain) throws ServletException, IOException {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
String token = header.substring(7);
|
||||
Authentication auth = jwtService.authenticate(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Yetkilendirme
|
||||
|
||||
- Method güvenliğini etkinleştirin: `@EnableMethodSecurity`
|
||||
- `@PreAuthorize("hasRole('ADMIN')")` veya `@PreAuthorize("@authz.canEdit(#id)")` kullanın
|
||||
- Varsayılan olarak reddedin; sadece gerekli scope'ları açığa çıkarın
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
public class AdminController {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@GetMapping("/users")
|
||||
public List<UserDto> listUsers() {
|
||||
return userService.findAll();
|
||||
}
|
||||
|
||||
@PreAuthorize("@authz.isOwner(#id, authentication)")
|
||||
@DeleteMapping("/users/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
userService.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Girdi Doğrulama
|
||||
|
||||
- Controller'larda `@Valid` ile Bean Validation kullanın
|
||||
- DTO'lara kısıtlamalar uygulayın: `@NotBlank`, `@Email`, `@Size`, custom validator'lar
|
||||
- Render etmeden önce herhangi bir HTML'i whitelist ile temizleyin
|
||||
|
||||
```java
|
||||
// KÖTÜ: Validation yok
|
||||
@PostMapping("/users")
|
||||
public User createUser(@RequestBody UserDto dto) {
|
||||
return userService.create(dto);
|
||||
}
|
||||
|
||||
// İYİ: Doğrulanmış DTO
|
||||
public record CreateUserDto(
|
||||
@NotBlank @Size(max = 100) String name,
|
||||
@NotBlank @Email String email,
|
||||
@NotNull @Min(0) @Max(150) Integer age
|
||||
) {}
|
||||
|
||||
@PostMapping("/users")
|
||||
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(userService.create(dto));
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Injection Önleme
|
||||
|
||||
- Spring Data repository'leri veya parametreli sorgular kullanın
|
||||
- Native sorgular için `:param` binding'leri kullanın; string'leri asla birleştirmeyin
|
||||
|
||||
```java
|
||||
// KÖTÜ: Native sorguda string birleştirme
|
||||
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||
|
||||
// İYİ: Parametreli native sorgu
|
||||
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
|
||||
List<User> findByName(@Param("name") String name);
|
||||
|
||||
// İYİ: Spring Data türetilmiş sorgu (otomatik parametreli)
|
||||
List<User> findByEmailAndActiveTrue(String email);
|
||||
```
|
||||
|
||||
## Parola Kodlama
|
||||
|
||||
- Parolaları her zaman BCrypt veya Argon2 ile hash'leyin — asla düz metin saklamayın
|
||||
- Manuel hash'leme değil `PasswordEncoder` bean'i kullanın
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder(12); // cost faktörü 12
|
||||
}
|
||||
|
||||
// Servis içinde
|
||||
public User register(CreateUserDto dto) {
|
||||
String hashedPassword = passwordEncoder.encode(dto.password());
|
||||
return userRepository.save(new User(dto.email(), hashedPassword));
|
||||
}
|
||||
```
|
||||
|
||||
## CSRF Koruması
|
||||
|
||||
- Tarayıcı session uygulamaları için CSRF'i etkin tutun; formlara/başlıklara token ekleyin
|
||||
- Bearer token'lı saf API'ler için CSRF'i devre dışı bırakın ve stateless auth'a güvenin
|
||||
|
||||
```java
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||
```
|
||||
|
||||
## Gizli Bilgi Yönetimi
|
||||
|
||||
- Kaynak kodda gizli bilgi yok; env veya vault'tan yükleyin
|
||||
- `application.yml`'i kimlik bilgilerinden arınmış tutun; yer tutucular kullanın
|
||||
- Token'ları ve DB kimlik bilgilerini düzenli olarak döndürün
|
||||
|
||||
```yaml
|
||||
# KÖTÜ: application.yml'de sabit kodlanmış
|
||||
spring:
|
||||
datasource:
|
||||
password: mySecretPassword123
|
||||
|
||||
# İYİ: Ortam değişkeni yer tutucu
|
||||
spring:
|
||||
datasource:
|
||||
password: ${DB_PASSWORD}
|
||||
|
||||
# İYİ: Spring Cloud Vault entegrasyonu
|
||||
spring:
|
||||
cloud:
|
||||
vault:
|
||||
uri: https://vault.example.com
|
||||
token: ${VAULT_TOKEN}
|
||||
```
|
||||
|
||||
## Güvenlik Başlıkları
|
||||
|
||||
```java
|
||||
http
|
||||
.headers(headers -> headers
|
||||
.contentSecurityPolicy(csp -> csp
|
||||
.policyDirectives("default-src 'self'"))
|
||||
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
|
||||
.xssProtection(Customizer.withDefaults())
|
||||
.referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)));
|
||||
```
|
||||
|
||||
## CORS Yapılandırması
|
||||
|
||||
- CORS'u controller başına değil, güvenlik filtre seviyesinde yapılandırın
|
||||
- İzin verilen origin'leri kısıtlayın — production'da asla `*` kullanmayın
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOrigins(List.of("https://app.example.com"));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
|
||||
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/api/**", config);
|
||||
return source;
|
||||
}
|
||||
|
||||
// SecurityFilterChain içinde:
|
||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Pahalı endpoint'lerde Bucket4j veya gateway seviyesi limitler uygulayın
|
||||
- Patlamalarda logla ve uyar; yeniden deneme ipuçları ile 429 döndür
|
||||
|
||||
```java
|
||||
// Endpoint başına rate limiting için Bucket4j kullanma
|
||||
@Component
|
||||
public class RateLimitFilter extends OncePerRequestFilter {
|
||||
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
||||
|
||||
private Bucket createBucket() {
|
||||
return Bucket.builder()
|
||||
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain chain) throws ServletException, IOException {
|
||||
String clientIp = request.getRemoteAddr();
|
||||
Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createBucket());
|
||||
|
||||
if (bucket.tryConsume(1)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bağımlılık Güvenliği
|
||||
|
||||
- CI'da OWASP Dependency Check / Snyk çalıştırın
|
||||
- Spring Boot ve Spring Security'yi desteklenen sürümlerde tutun
|
||||
- Bilinen CVE'lerde build'leri başarısız yapın
|
||||
|
||||
## Loglama ve PII
|
||||
|
||||
- Gizli bilgileri, token'ları, parolaları veya tam PAN verilerini asla loglamayın
|
||||
- Hassas alanları redakte edin; yapılandırılmış JSON loglama kullanın
|
||||
|
||||
## Dosya Yüklemeleri
|
||||
|
||||
- Boyutu, content type'ı ve uzantıyı doğrulayın
|
||||
- Web root dışında saklayın; gerekirse tarayın
|
||||
|
||||
## Yayın Öncesi Kontrol Listesi
|
||||
|
||||
- [ ] Auth token'ları doğru şekilde doğrulanmış ve süresi dolmuş
|
||||
- [ ] Her hassas path'te yetkilendirme korumaları
|
||||
- [ ] Tüm girişler doğrulanmış ve temizlenmiş
|
||||
- [ ] String-birleştirilmiş SQL yok
|
||||
- [ ] Uygulama türü için doğru CSRF duruşu
|
||||
- [ ] Gizli bilgiler harici; hiçbiri commit edilmemiş
|
||||
- [ ] Güvenlik başlıkları yapılandırılmış
|
||||
- [ ] API'lerde rate limiting
|
||||
- [ ] Bağımlılıklar taranmış ve güncel
|
||||
- [ ] Loglar hassas verilerden arınmış
|
||||
|
||||
**Unutmayın**: Varsayılan olarak reddet, girişleri doğrula, en az ayrıcalık ve önce yapılandırma ile güvenli.
|
||||
158
docs/tr/skills/springboot-tdd/SKILL.md
Normal file
158
docs/tr/skills/springboot-tdd/SKILL.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: springboot-tdd
|
||||
description: Test-driven development for Spring Boot using JUnit 5, Mockito, MockMvc, Testcontainers, and JaCoCo. Use when adding features, fixing bugs, or refactoring.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Spring Boot TDD İş Akışı
|
||||
|
||||
80%+ kapsam (unit + integration) ile Spring Boot servisleri için TDD rehberi.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
- Yeni özellikler veya endpoint'ler
|
||||
- Bug düzeltmeleri veya refactoring'ler
|
||||
- Veri erişim mantığı veya güvenlik kuralları ekleme
|
||||
|
||||
## İş Akışı
|
||||
|
||||
1) Önce testleri yazın (başarısız olmalılar)
|
||||
2) Geçmek için minimal kod uygulayın
|
||||
3) Testleri yeşil tutarken refactor edin
|
||||
4) Kapsamı zorlayın (JaCoCo)
|
||||
|
||||
## Unit Testler (JUnit 5 + Mockito)
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MarketServiceTest {
|
||||
@Mock MarketRepository repo;
|
||||
@InjectMocks MarketService service;
|
||||
|
||||
@Test
|
||||
void createsMarket() {
|
||||
CreateMarketRequest req = new CreateMarketRequest("name", "desc", Instant.now(), List.of("cat"));
|
||||
when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
Market result = service.create(req);
|
||||
|
||||
assertThat(result.name()).isEqualTo("name");
|
||||
verify(repo).save(any());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Desenler:
|
||||
- Arrange-Act-Assert
|
||||
- Kısmi mock'lardan kaçının; açık stubbing tercih edin
|
||||
- Varyantlar için `@ParameterizedTest` kullanın
|
||||
|
||||
## Web Katmanı Testleri (MockMvc)
|
||||
|
||||
```java
|
||||
@WebMvcTest(MarketController.class)
|
||||
class MarketControllerTest {
|
||||
@Autowired MockMvc mockMvc;
|
||||
@MockBean MarketService marketService;
|
||||
|
||||
@Test
|
||||
void returnsMarkets() throws Exception {
|
||||
when(marketService.list(any())).thenReturn(Page.empty());
|
||||
|
||||
mockMvc.perform(get("/api/markets"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.content").isArray());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Entegrasyon Testleri (SpringBootTest)
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class MarketIntegrationTest {
|
||||
@Autowired MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void createsMarket() throws Exception {
|
||||
mockMvc.perform(post("/api/markets")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name":"Test","description":"Desc","endDate":"2030-01-01T00:00:00Z","categories":["general"]}
|
||||
"""))
|
||||
.andExpect(status().isCreated());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Persistence Testleri (DataJpaTest)
|
||||
|
||||
```java
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import(TestContainersConfig.class)
|
||||
class MarketRepositoryTest {
|
||||
@Autowired MarketRepository repo;
|
||||
|
||||
@Test
|
||||
void savesAndFinds() {
|
||||
MarketEntity entity = new MarketEntity();
|
||||
entity.setName("Test");
|
||||
repo.save(entity);
|
||||
|
||||
Optional<MarketEntity> found = repo.findByName("Test");
|
||||
assertThat(found).isPresent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testcontainers
|
||||
|
||||
- Production'ı yansıtmak için Postgres/Redis için yeniden kullanılabilir container'lar kullanın
|
||||
- JDBC URL'lerini Spring context'e enjekte etmek için `@DynamicPropertySource` ile bağlayın
|
||||
|
||||
## Kapsam (JaCoCo)
|
||||
|
||||
Maven snippet:
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.14</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals><goal>prepare-agent</goal></goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>verify</phase>
|
||||
<goals><goal>report</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
## Assertion'lar
|
||||
|
||||
- Okunabilirlik için AssertJ'yi (`assertThat`) tercih edin
|
||||
- JSON yanıtları için `jsonPath` kullanın
|
||||
- Exception'lar için: `assertThatThrownBy(...)`
|
||||
|
||||
## Test Veri Builder'ları
|
||||
|
||||
```java
|
||||
class MarketBuilder {
|
||||
private String name = "Test";
|
||||
MarketBuilder withName(String name) { this.name = name; return this; }
|
||||
Market build() { return new Market(null, name, MarketStatus.ACTIVE); }
|
||||
}
|
||||
```
|
||||
|
||||
## CI Komutları
|
||||
|
||||
- Maven: `mvn -T 4 test` veya `mvn verify`
|
||||
- Gradle: `./gradlew test jacocoTestReport`
|
||||
|
||||
**Unutmayın**: Testleri hızlı, izole ve deterministik tutun. Uygulama detaylarını değil, davranışı test edin.
|
||||
231
docs/tr/skills/springboot-verification/SKILL.md
Normal file
231
docs/tr/skills/springboot-verification/SKILL.md
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
name: springboot-verification
|
||||
description: "Verification loop for Spring Boot projects: build, static analysis, tests with coverage, security scans, and diff review before release or PR."
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Spring Boot Doğrulama Döngüsü
|
||||
|
||||
PR'lardan önce, büyük değişikliklerden sonra ve deployment öncesi çalıştırın.
|
||||
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
- Spring Boot servisi için pull request açmadan önce
|
||||
- Büyük refactoring veya bağımlılık yükseltmelerinden sonra
|
||||
- Staging veya production için deployment öncesi doğrulama
|
||||
- Tam build → lint → test → güvenlik taraması pipeline'ı çalıştırma
|
||||
- Test kapsamının eşikleri karşıladığını doğrulama
|
||||
|
||||
## Faz 1: Build
|
||||
|
||||
```bash
|
||||
mvn -T 4 clean verify -DskipTests
|
||||
# veya
|
||||
./gradlew clean assemble -x test
|
||||
```
|
||||
|
||||
Build başarısız olursa, durdurun ve düzeltin.
|
||||
|
||||
## Faz 2: Static Analiz
|
||||
|
||||
Maven (yaygın plugin'ler):
|
||||
```bash
|
||||
mvn -T 4 spotbugs:check pmd:check checkstyle:check
|
||||
```
|
||||
|
||||
Gradle (yapılandırılmışsa):
|
||||
```bash
|
||||
./gradlew checkstyleMain pmdMain spotbugsMain
|
||||
```
|
||||
|
||||
## Faz 3: Testler + Kapsam
|
||||
|
||||
```bash
|
||||
mvn -T 4 test
|
||||
mvn jacoco:report # 80%+ kapsam doğrula
|
||||
# veya
|
||||
./gradlew test jacocoTestReport
|
||||
```
|
||||
|
||||
Rapor:
|
||||
- Toplam testler, geçen/başarısız
|
||||
- Kapsam % (satırlar/dallar)
|
||||
|
||||
### Unit Testler
|
||||
|
||||
Mock bağımlılıklarla izole olarak servis mantığını test edin:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceTest {
|
||||
|
||||
@Mock private UserRepository userRepository;
|
||||
@InjectMocks private UserService userService;
|
||||
|
||||
@Test
|
||||
void createUser_validInput_returnsUser() {
|
||||
var dto = new CreateUserDto("Alice", "alice@example.com");
|
||||
var expected = new User(1L, "Alice", "alice@example.com");
|
||||
when(userRepository.save(any(User.class))).thenReturn(expected);
|
||||
|
||||
var result = userService.create(dto);
|
||||
|
||||
assertThat(result.name()).isEqualTo("Alice");
|
||||
verify(userRepository).save(any(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_duplicateEmail_throwsException() {
|
||||
var dto = new CreateUserDto("Alice", "existing@example.com");
|
||||
when(userRepository.existsByEmail(dto.email())).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> userService.create(dto))
|
||||
.isInstanceOf(DuplicateEmailException.class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testcontainers ile Entegrasyon Testleri
|
||||
|
||||
H2 yerine gerçek bir veritabanına karşı test edin:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
class UserRepositoryIntegrationTest {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
|
||||
.withDatabaseName("testdb");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void configureProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", postgres::getUsername);
|
||||
registry.add("spring.datasource.password", postgres::getPassword);
|
||||
}
|
||||
|
||||
@Autowired private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
void findByEmail_existingUser_returnsUser() {
|
||||
userRepository.save(new User("Alice", "alice@example.com"));
|
||||
|
||||
var found = userRepository.findByEmail("alice@example.com");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getName()).isEqualTo("Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MockMvc ile API Testleri
|
||||
|
||||
Tam Spring context ile controller katmanını test edin:
|
||||
|
||||
```java
|
||||
@WebMvcTest(UserController.class)
|
||||
class UserControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
@MockBean private UserService userService;
|
||||
|
||||
@Test
|
||||
void createUser_validInput_returns201() throws Exception {
|
||||
var user = new UserDto(1L, "Alice", "alice@example.com");
|
||||
when(userService.create(any())).thenReturn(user);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name": "Alice", "email": "alice@example.com"}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_invalidEmail_returns400() throws Exception {
|
||||
mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name": "Alice", "email": "not-an-email"}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Faz 4: Güvenlik Taraması
|
||||
|
||||
```bash
|
||||
# Bağımlılık CVE'leri
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
# veya
|
||||
./gradlew dependencyCheckAnalyze
|
||||
|
||||
# Kaynakta gizli bilgiler
|
||||
grep -rn "password\s*=\s*\"" src/ --include="*.java" --include="*.yml" --include="*.properties"
|
||||
grep -rn "sk-\|api_key\|secret" src/ --include="*.java" --include="*.yml"
|
||||
|
||||
# Gizli bilgiler (git geçmişi)
|
||||
git secrets --scan # yapılandırılmışsa
|
||||
```
|
||||
|
||||
### Yaygın Güvenlik Bulguları
|
||||
|
||||
```
|
||||
# System.out.println kontrolü (yerine logger kullan)
|
||||
grep -rn "System\.out\.print" src/main/ --include="*.java"
|
||||
|
||||
# Yanıtlarda ham exception mesajları kontrolü
|
||||
grep -rn "e\.getMessage()" src/main/ --include="*.java"
|
||||
|
||||
# Wildcard CORS kontrolü
|
||||
grep -rn "allowedOrigins.*\*" src/main/ --include="*.java"
|
||||
```
|
||||
|
||||
## Faz 5: Lint/Format (opsiyonel kapı)
|
||||
|
||||
```bash
|
||||
mvn spotless:apply # Spotless plugin kullanıyorsanız
|
||||
./gradlew spotlessApply
|
||||
```
|
||||
|
||||
## Faz 6: Diff İncelemesi
|
||||
|
||||
```bash
|
||||
git diff --stat
|
||||
git diff
|
||||
```
|
||||
|
||||
Kontrol listesi:
|
||||
- Debug logları kalmamış (`System.out`, koruma olmadan `log.debug`)
|
||||
- Anlamlı hatalar ve HTTP durumları
|
||||
- Gerekli yerlerde transaction'lar ve validation mevcut
|
||||
- Config değişiklikleri belgelenmiş
|
||||
|
||||
## Çıktı Şablonu
|
||||
|
||||
```
|
||||
DOĞRULAMA RAPORU
|
||||
===================
|
||||
Build: [GEÇTİ/BAŞARISIZ]
|
||||
Static: [GEÇTİ/BAŞARISIZ] (spotbugs/pmd/checkstyle)
|
||||
Testler: [GEÇTİ/BAŞARISIZ] (X/Y geçti, Z% kapsam)
|
||||
Güvenlik: [GEÇTİ/BAŞARISIZ] (CVE bulguları: N)
|
||||
Diff: [X dosya değişti]
|
||||
|
||||
Genel: [HAZIR / HAZIR DEĞİL]
|
||||
|
||||
Düzeltilecek Sorunlar:
|
||||
1. ...
|
||||
2. ...
|
||||
```
|
||||
|
||||
## Sürekli Mod
|
||||
|
||||
- Önemli değişikliklerde veya uzun oturumlarda her 30-60 dakikada bir fazları yeniden çalıştırın
|
||||
- Kısa döngü tutun: hızlı geri bildirim için `mvn -T 4 test` + spotbugs
|
||||
|
||||
**Unutmayın**: Hızlı geri bildirim geç sürprizleri yener. Kapıyı sıkı tutun—production sistemlerinde uyarıları kusur olarak değerlendirin.
|
||||
410
docs/tr/skills/tdd-workflow/SKILL.md
Normal file
410
docs/tr/skills/tdd-workflow/SKILL.md
Normal file
@@ -0,0 +1,410 @@
|
||||
---
|
||||
name: tdd-workflow
|
||||
description: Yeni özellikler yazarken, hata düzeltirken veya kod refactor ederken bu skill'i kullanın. Unit, integration ve E2E testlerini içeren %80+ kapsam ile test güdümlü geliştirmeyi zorlar.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Test Güdümlü Geliştirme İş Akışı
|
||||
|
||||
Bu skill tüm kod geliştirmenin kapsamlı test kapsamı ile TDD ilkelerini takip etmesini sağlar.
|
||||
|
||||
## Ne Zaman Aktifleştirmelisiniz
|
||||
|
||||
- Yeni özellikler veya fonksiyonellik yazarken
|
||||
- Hataları veya sorunları düzeltirken
|
||||
- Mevcut kodu refactor ederken
|
||||
- API endpoint'leri eklerken
|
||||
- Yeni bileşenler oluştururken
|
||||
|
||||
## Temel İlkeler
|
||||
|
||||
### 1. Koddan ÖNCE Testler
|
||||
HER ZAMAN önce testleri yazın, sonra testleri geçmesi için kod uygulayın.
|
||||
|
||||
### 2. Kapsam Gereksinimleri
|
||||
- Minimum %80 kapsam (unit + integration + E2E)
|
||||
- Tüm uç durumlar kapsanmış
|
||||
- Hata senaryoları test edilmiş
|
||||
- Sınır koşulları doğrulanmış
|
||||
|
||||
### 3. Test Tipleri
|
||||
|
||||
#### Unit Testler
|
||||
- Bireysel fonksiyonlar ve yardımcı araçlar
|
||||
- Bileşen mantığı
|
||||
- Pure fonksiyonlar
|
||||
- Yardımcılar ve utilities
|
||||
|
||||
#### Integration Testler
|
||||
- API endpoint'leri
|
||||
- Veritabanı operasyonları
|
||||
- Service etkileşimleri
|
||||
- Harici API çağrıları
|
||||
|
||||
#### E2E Testler (Playwright)
|
||||
- Kritik kullanıcı akışları
|
||||
- Tam iş akışları
|
||||
- Tarayıcı otomasyonu
|
||||
- UI etkileşimleri
|
||||
|
||||
## TDD İş Akışı Adımları
|
||||
|
||||
### Adım 1: Kullanıcı Hikayeleri Yazın
|
||||
```
|
||||
[Rol] olarak, [eylem] yapmak istiyorum, böylece [fayda] elde ederim
|
||||
|
||||
Örnek:
|
||||
Kullanıcı olarak, marketleri semantik olarak aramak istiyorum,
|
||||
böylece tam anahtar kelimeler olmasa bile ilgili marketleri bulabilirim.
|
||||
```
|
||||
|
||||
### Adım 2: Test Senaryoları Oluşturun
|
||||
Her kullanıcı hikayesi için kapsamlı test senaryoları oluşturun:
|
||||
|
||||
```typescript
|
||||
describe('Semantik Arama', () => {
|
||||
it('sorgu için ilgili marketleri döndürür', async () => {
|
||||
// Test implementasyonu
|
||||
})
|
||||
|
||||
it('boş sorguyu zarif şekilde işler', async () => {
|
||||
// Uç durumu test et
|
||||
})
|
||||
|
||||
it('Redis kullanılamazsa substring aramaya geri döner', async () => {
|
||||
// Fallback davranışını test et
|
||||
})
|
||||
|
||||
it('sonuçları benzerlik skoruna göre sıralar', async () => {
|
||||
// Sıralama mantığını test et
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Adım 3: Testleri Çalıştırın (Başarısız Olmalı)
|
||||
```bash
|
||||
npm test
|
||||
# Testler başarısız olmalı - henüz implement etmedik
|
||||
```
|
||||
|
||||
### Adım 4: Kod Uygulayın
|
||||
Testleri geçmesi için minimal kod yazın:
|
||||
|
||||
```typescript
|
||||
// Testler tarafından yönlendirilen implementasyon
|
||||
export async function searchMarkets(query: string) {
|
||||
// Implementasyon buraya
|
||||
}
|
||||
```
|
||||
|
||||
### Adım 5: Testleri Tekrar Çalıştırın
|
||||
```bash
|
||||
npm test
|
||||
# Testler artık geçmeli
|
||||
```
|
||||
|
||||
### Adım 6: Refactor Edin
|
||||
Testleri yeşil tutarken kod kalitesini iyileştirin:
|
||||
- Tekrarı kaldırın
|
||||
- İsimlendirmeyi iyileştirin
|
||||
- Performansı optimize edin
|
||||
- Okunabilirliği artırın
|
||||
|
||||
### Adım 7: Kapsamı Doğrulayın
|
||||
```bash
|
||||
npm run test:coverage
|
||||
# %80+ kapsam sağlandığını doğrula
|
||||
```
|
||||
|
||||
## Test Kalıpları
|
||||
|
||||
### Unit Test Kalıbı (Jest/Vitest)
|
||||
```typescript
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { Button } from './Button'
|
||||
|
||||
describe('Button Bileşeni', () => {
|
||||
it('doğru metinle render eder', () => {
|
||||
render(<Button>Tıkla</Button>)
|
||||
expect(screen.getByText('Tıkla')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('tıklandığında onClick\'i çağırır', () => {
|
||||
const handleClick = jest.fn()
|
||||
render(<Button onClick={handleClick}>Tıkla</Button>)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('disabled prop true olduğunda devre dışı kalır', () => {
|
||||
render(<Button disabled>Tıkla</Button>)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### API Integration Test Kalıbı
|
||||
```typescript
|
||||
import { NextRequest } from 'next/server'
|
||||
import { GET } from './route'
|
||||
|
||||
describe('GET /api/markets', () => {
|
||||
it('marketleri başarıyla döndürür', async () => {
|
||||
const request = new NextRequest('http://localhost/api/markets')
|
||||
const response = await GET(request)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(Array.isArray(data.data)).toBe(true)
|
||||
})
|
||||
|
||||
it('query parametrelerini validate eder', async () => {
|
||||
const request = new NextRequest('http://localhost/api/markets?limit=invalid')
|
||||
const response = await GET(request)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
it('veritabanı hatalarını zarif şekilde işler', async () => {
|
||||
// Veritabanı başarısızlığını mock'la
|
||||
const request = new NextRequest('http://localhost/api/markets')
|
||||
// Hata işlemeyi test et
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E Test Kalıbı (Playwright)
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('kullanıcı marketleri arayabilir ve filtreleyebilir', async ({ page }) => {
|
||||
// Markets sayfasına git
|
||||
await page.goto('/')
|
||||
await page.click('a[href="/markets"]')
|
||||
|
||||
// Sayfanın yüklendiğini doğrula
|
||||
await expect(page.locator('h1')).toContainText('Markets')
|
||||
|
||||
// Marketleri ara
|
||||
await page.fill('input[placeholder="Marketleri ara"]', 'election')
|
||||
|
||||
// Debounce ve sonuçları bekle
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
// Arama sonuçlarının gösterildiğini doğrula
|
||||
const results = page.locator('[data-testid="market-card"]')
|
||||
await expect(results).toHaveCount(5, { timeout: 5000 })
|
||||
|
||||
// Sonuçların arama terimini içerdiğini doğrula
|
||||
const firstResult = results.first()
|
||||
await expect(firstResult).toContainText('election', { ignoreCase: true })
|
||||
|
||||
// Duruma göre filtrele
|
||||
await page.click('button:has-text("Aktif")')
|
||||
|
||||
// Filtrelenmiş sonuçları doğrula
|
||||
await expect(results).toHaveCount(3)
|
||||
})
|
||||
|
||||
test('kullanıcı yeni market oluşturabilir', async ({ page }) => {
|
||||
// Önce login ol
|
||||
await page.goto('/creator-dashboard')
|
||||
|
||||
// Market oluşturma formunu doldur
|
||||
await page.fill('input[name="name"]', 'Test Market')
|
||||
await page.fill('textarea[name="description"]', 'Test açıklama')
|
||||
await page.fill('input[name="endDate"]', '2025-12-31')
|
||||
|
||||
// Formu gönder
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
// Başarı mesajını doğrula
|
||||
await expect(page.locator('text=Market başarıyla oluşturuldu')).toBeVisible()
|
||||
|
||||
// Market sayfasına yönlendirmeyi doğrula
|
||||
await expect(page).toHaveURL(/\/markets\/test-market/)
|
||||
})
|
||||
```
|
||||
|
||||
## Test Dosya Organizasyonu
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Button/
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Button.test.tsx # Unit testler
|
||||
│ │ └── Button.stories.tsx # Storybook
|
||||
│ └── MarketCard/
|
||||
│ ├── MarketCard.tsx
|
||||
│ └── MarketCard.test.tsx
|
||||
├── app/
|
||||
│ └── api/
|
||||
│ └── markets/
|
||||
│ ├── route.ts
|
||||
│ └── route.test.ts # Integration testler
|
||||
└── e2e/
|
||||
├── markets.spec.ts # E2E testler
|
||||
├── trading.spec.ts
|
||||
└── auth.spec.ts
|
||||
```
|
||||
|
||||
## Harici Servisleri Mock'lama
|
||||
|
||||
### Supabase Mock
|
||||
```typescript
|
||||
jest.mock('@/lib/supabase', () => ({
|
||||
supabase: {
|
||||
from: jest.fn(() => ({
|
||||
select: jest.fn(() => ({
|
||||
eq: jest.fn(() => Promise.resolve({
|
||||
data: [{ id: 1, name: 'Test Market' }],
|
||||
error: null
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
### Redis Mock
|
||||
```typescript
|
||||
jest.mock('@/lib/redis', () => ({
|
||||
searchMarketsByVector: jest.fn(() => Promise.resolve([
|
||||
{ slug: 'test-market', similarity_score: 0.95 }
|
||||
])),
|
||||
checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
|
||||
}))
|
||||
```
|
||||
|
||||
### OpenAI Mock
|
||||
```typescript
|
||||
jest.mock('@/lib/openai', () => ({
|
||||
generateEmbedding: jest.fn(() => Promise.resolve(
|
||||
new Array(1536).fill(0.1) // Mock 1536-boyutlu embedding
|
||||
))
|
||||
}))
|
||||
```
|
||||
|
||||
## Test Kapsamı Doğrulama
|
||||
|
||||
### Kapsam Raporu Çalıştır
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Kapsam Eşikleri
|
||||
```json
|
||||
{
|
||||
"jest": {
|
||||
"coverageThresholds": {
|
||||
"global": {
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kaçınılması Gereken Yaygın Test Hataları
|
||||
|
||||
### ❌ YANLIŞ: Implementasyon Detaylarını Test Etme
|
||||
```typescript
|
||||
// İç state'i test etme
|
||||
expect(component.state.count).toBe(5)
|
||||
```
|
||||
|
||||
### ✅ DOĞRU: Kullanıcı Tarafından Görünen Davranışı Test Et
|
||||
```typescript
|
||||
// Kullanıcıların gördüğünü test et
|
||||
expect(screen.getByText('Sayı: 5')).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### ❌ YANLIŞ: Kırılgan Selector'lar
|
||||
```typescript
|
||||
// Kolayca bozulur
|
||||
await page.click('.css-class-xyz')
|
||||
```
|
||||
|
||||
### ✅ DOĞRU: Semantik Selector'lar
|
||||
```typescript
|
||||
// Değişikliklere karşı dayanıklı
|
||||
await page.click('button:has-text("Gönder")')
|
||||
await page.click('[data-testid="submit-button"]')
|
||||
```
|
||||
|
||||
### ❌ YANLIŞ: Test İzolasyonu Yok
|
||||
```typescript
|
||||
// Testler birbirine bağımlı
|
||||
test('kullanıcı oluşturur', () => { /* ... */ })
|
||||
test('aynı kullanıcıyı günceller', () => { /* önceki teste bağımlı */ })
|
||||
```
|
||||
|
||||
### ✅ DOĞRU: Bağımsız Testler
|
||||
```typescript
|
||||
// Her test kendi verisini hazırlar
|
||||
test('kullanıcı oluşturur', () => {
|
||||
const user = createTestUser()
|
||||
// Test mantığı
|
||||
})
|
||||
|
||||
test('kullanıcı günceller', () => {
|
||||
const user = createTestUser()
|
||||
// Güncelleme mantığı
|
||||
})
|
||||
```
|
||||
|
||||
## Sürekli Test
|
||||
|
||||
### Geliştirme Sırasında Watch Modu
|
||||
```bash
|
||||
npm test -- --watch
|
||||
# Dosya değişikliklerinde testler otomatik çalışır
|
||||
```
|
||||
|
||||
### Pre-Commit Hook
|
||||
```bash
|
||||
# Her commit öncesi çalışır
|
||||
npm test && npm run lint
|
||||
```
|
||||
|
||||
### CI/CD Entegrasyonu
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Run Tests
|
||||
run: npm test -- --coverage
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
1. **Önce Testleri Yaz** - Her zaman TDD
|
||||
2. **Test Başına Bir Assert** - Tek davranışa odaklan
|
||||
3. **Açıklayıcı Test İsimleri** - Neyin test edildiğini açıkla
|
||||
4. **Arrange-Act-Assert** - Net test yapısı
|
||||
5. **Harici Bağımlılıkları Mock'la** - Unit testleri izole et
|
||||
6. **Uç Durumları Test Et** - Null, undefined, boş, büyük
|
||||
7. **Hata Yollarını Test Et** - Sadece happy path değil
|
||||
8. **Testleri Hızlı Tut** - Unit testler < 50ms her biri
|
||||
9. **Testlerden Sonra Temizle** - Yan etki yok
|
||||
10. **Kapsam Raporlarını İncele** - Boşlukları tespit et
|
||||
|
||||
## Başarı Metrikleri
|
||||
|
||||
- %80+ kod kapsamı sağlanmış
|
||||
- Tüm testler geçiyor (yeşil)
|
||||
- Atlanmış veya devre dışı test yok
|
||||
- Hızlı test yürütme (< 30s unit testler için)
|
||||
- E2E testler kritik kullanıcı akışlarını kapsıyor
|
||||
- Testler production'dan önce hataları yakalar
|
||||
|
||||
---
|
||||
|
||||
**Unutmayın**: Testler opsiyonel değildir. Güvenli refactoring, hızlı geliştirme ve production güvenilirliği sağlayan güvenlik ağıdırlar.
|
||||
126
docs/tr/skills/verification-loop/SKILL.md
Normal file
126
docs/tr/skills/verification-loop/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: verification-loop
|
||||
description: "Claude Code oturumları için kapsamlı doğrulama sistemi."
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Verification Loop Skill
|
||||
|
||||
Claude Code oturumları için kapsamlı doğrulama sistemi.
|
||||
|
||||
## Ne Zaman Kullanılır
|
||||
|
||||
Bu skill'i şu durumlarda çağır:
|
||||
- Bir özellik veya önemli kod değişikliği tamamladıktan sonra
|
||||
- PR oluşturmadan önce
|
||||
- Kalite kapılarının geçtiğinden emin olmak istediğinde
|
||||
- Refactoring sonrasında
|
||||
|
||||
## Doğrulama Fazları
|
||||
|
||||
### Faz 1: Build Doğrulaması
|
||||
```bash
|
||||
# Projenin build olup olmadığını kontrol et
|
||||
npm run build 2>&1 | tail -20
|
||||
# VEYA
|
||||
pnpm build 2>&1 | tail -20
|
||||
```
|
||||
|
||||
Build başarısız olursa, devam etmeden önce DUR ve düzelt.
|
||||
|
||||
### Faz 2: Tip Kontrolü
|
||||
```bash
|
||||
# TypeScript projeleri
|
||||
npx tsc --noEmit 2>&1 | head -30
|
||||
|
||||
# Python projeleri
|
||||
pyright . 2>&1 | head -30
|
||||
```
|
||||
|
||||
Tüm tip hatalarını raporla. Devam etmeden önce kritik olanları düzelt.
|
||||
|
||||
### Faz 3: Lint Kontrolü
|
||||
```bash
|
||||
# JavaScript/TypeScript
|
||||
npm run lint 2>&1 | head -30
|
||||
|
||||
# Python
|
||||
ruff check . 2>&1 | head -30
|
||||
```
|
||||
|
||||
### Faz 4: Test Paketi
|
||||
```bash
|
||||
# Testleri coverage ile çalıştır
|
||||
npm run test -- --coverage 2>&1 | tail -50
|
||||
|
||||
# Coverage eşiğini kontrol et
|
||||
# Hedef: minimum %80
|
||||
```
|
||||
|
||||
Rapor:
|
||||
- Toplam testler: X
|
||||
- Geçti: X
|
||||
- Başarısız: X
|
||||
- Coverage: %X
|
||||
|
||||
### Faz 5: Güvenlik Taraması
|
||||
```bash
|
||||
# Secret'ları kontrol et
|
||||
grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
|
||||
grep -rn "api_key" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
|
||||
|
||||
# console.log kontrol et
|
||||
grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10
|
||||
```
|
||||
|
||||
### Faz 6: Diff İncelemesi
|
||||
```bash
|
||||
# Neyin değiştiğini göster
|
||||
git diff --stat
|
||||
git diff HEAD~1 --name-only
|
||||
```
|
||||
|
||||
Her değişen dosyayı şunlar için incele:
|
||||
- İstenmeyen değişiklikler
|
||||
- Eksik hata işleme
|
||||
- Potansiyel edge case'ler
|
||||
|
||||
## Çıktı Formatı
|
||||
|
||||
Tüm fazları çalıştırdıktan sonra, bir doğrulama raporu üret:
|
||||
|
||||
```
|
||||
DOĞRULAMA RAPORU
|
||||
==================
|
||||
|
||||
Build: [PASS/FAIL]
|
||||
Tipler: [PASS/FAIL] (X hata)
|
||||
Lint: [PASS/FAIL] (X uyarı)
|
||||
Testler: [PASS/FAIL] (X/Y geçti, %Z coverage)
|
||||
Güvenlik: [PASS/FAIL] (X sorun)
|
||||
Diff: [X dosya değişti]
|
||||
|
||||
Genel: PR için [HAZIR/HAZIR DEĞİL]
|
||||
|
||||
Düzeltilmesi Gereken Sorunlar:
|
||||
1. ...
|
||||
2. ...
|
||||
```
|
||||
|
||||
## Sürekli Mod
|
||||
|
||||
Uzun oturumlar için, her 15 dakikada bir veya major değişikliklerden sonra doğrulama çalıştır:
|
||||
|
||||
```markdown
|
||||
Mental kontrol noktası belirle:
|
||||
- Her fonksiyonu tamamladıktan sonra
|
||||
- Bir component'i bitirdikten sonra
|
||||
- Sonraki göreve geçmeden önce
|
||||
|
||||
Çalıştır: /verify
|
||||
```
|
||||
|
||||
## Hook'larla Entegrasyon
|
||||
|
||||
Bu skill PostToolUse hook'larını tamamlar ancak daha derin doğrulama sağlar.
|
||||
Hook'lar sorunları anında yakalar; bu skill kapsamlı inceleme sağlar.
|
||||
Reference in New Issue
Block a user