Skip to content

Commit 9a7c0be

Browse files
feat: rich change detail display with before/after comparison
Differ artik her degisiklik icin detayli bilgi uretiyor: change_type (okunabilir etiket), old_value/new_value (field ozeti), old_preview/new_preview (JSON schema onizleme), description (etki aciklamasi). Change detail sayfasinda expand/collapse kartlar, before/after karsilastirma, schema detail onizleme eklendi. Changes listesinde her satira degisiklik rozetleri eklendi. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 158bb3a commit 9a7c0be

3 files changed

Lines changed: 303 additions & 66 deletions

File tree

schema-monitor/core/differ.py

Lines changed: 166 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Schema karşılaştırma modülü — iki schema arasındaki farkları bulur."""
22

3+
import json
34
import logging
45
from datetime import datetime, timezone
56
from typing import Any
@@ -10,6 +11,121 @@
1011

1112
logger = logging.getLogger(__name__)
1213

14+
# Değişiklik tipi → okunabilir etiket ve açıklama eşlemesi
15+
_CHANGE_TYPE_LABELS: dict[str, str] = {
16+
"field_removed": "Field Removed",
17+
"field_added": "Field Added",
18+
"type_changed": "Type Changed",
19+
"nullable_changed_to_false": "Nullable Changed",
20+
"array_item_schema_changed": "Array Schema Changed",
21+
"auth_changed": "Auth Changed",
22+
"format_changed": "Format Changed",
23+
"enum_value_removed": "Enum Value Removed",
24+
"enum_value_added": "Enum Value Added",
25+
"required_field_added": "Required Field Added",
26+
"depth_changed": "Depth Changed",
27+
"status_code_changed_2xx": "Status Code Changed",
28+
}
29+
30+
_CHANGE_TYPE_DESCRIPTIONS: dict[str, str] = {
31+
"field_removed": "Bu alan API response'undan tamamen kaldirildi. Bu alana bagli istemciler hata alabilir.",
32+
"field_added": "API response'una yeni bir alan eklendi. Mevcut istemciler etkilenmez.",
33+
"type_changed": "Bu alanin veri tipi degisti. Tip uyumsuzlugu istemcilerde parse hatasina yol acabilir.",
34+
"nullable_changed_to_false": "Bu alan artik null deger kabul etmiyor. null bekleyen istemciler etkilenebilir.",
35+
"array_item_schema_changed": "Array icindeki nesnelerin yapisi degisti. Listelemeye bagli istemciler etkilenebilir.",
36+
"auth_changed": "Kimlik dogrulama yapisi degisti.",
37+
"format_changed": "Alanin formati degisti (ornek: date -> datetime).",
38+
"enum_value_removed": "Izin verilen degerlerden biri kaldirildi.",
39+
"enum_value_added": "Izin verilen degerlere yeni bir deger eklendi.",
40+
"required_field_added": "Yeni bir zorunlu alan eklendi.",
41+
"depth_changed": "Nesnelerin icleme derinligi degisti.",
42+
"status_code_changed_2xx": "HTTP basari durum kodu degisti.",
43+
}
44+
45+
46+
def _field_summary(field_data: dict[str, Any]) -> str:
47+
"""Bir field'ın kısa yapısal özetini oluştur.
48+
49+
Args:
50+
field_data: Schema field verisi
51+
52+
Returns:
53+
Okunabilir field özeti
54+
"""
55+
parts: list[str] = []
56+
57+
field_type = field_data.get("type", "unknown")
58+
parts.append(f"type: {field_type}")
59+
60+
if field_data.get("nullable"):
61+
parts.append("nullable: true")
62+
63+
if field_type == "object" and "fields" in field_data:
64+
sub_fields = list(field_data["fields"].keys())
65+
if sub_fields:
66+
if len(sub_fields) <= 5:
67+
parts.append(f"fields: [{', '.join(sub_fields)}]")
68+
else:
69+
parts.append(f"fields: [{', '.join(sub_fields[:5])}, ... +{len(sub_fields) - 5}]")
70+
71+
if field_type == "array" and "items" in field_data:
72+
items = field_data["items"]
73+
items_type = items.get("type", "unknown")
74+
if items_type == "object" and "fields" in items:
75+
item_fields = list(items["fields"].keys())
76+
if len(item_fields) <= 5:
77+
parts.append(f"items: {items_type} [{', '.join(item_fields)}]")
78+
else:
79+
parts.append(f"items: {items_type} [{', '.join(item_fields[:5])}, ... +{len(item_fields) - 5}]")
80+
else:
81+
parts.append(f"items: {items_type}")
82+
83+
if "sample" in field_data:
84+
sample = str(field_data["sample"])
85+
if len(sample) > 50:
86+
sample = sample[:50] + "..."
87+
parts.append(f'sample: "{sample}"')
88+
89+
return "{ " + ", ".join(parts) + " }"
90+
91+
92+
def _field_to_json_preview(field_data: dict[str, Any], max_depth: int = 2) -> dict[str, Any]:
93+
"""Bir field'ın JSON benzeri önizlemesini oluştur.
94+
95+
Args:
96+
field_data: Schema field verisi
97+
max_depth: Maksimum derinlik
98+
99+
Returns:
100+
JSON preview dict
101+
"""
102+
result: dict[str, Any] = {"type": field_data.get("type", "unknown")}
103+
104+
if field_data.get("nullable"):
105+
result["nullable"] = True
106+
107+
if "sample" in field_data:
108+
result["sample"] = field_data["sample"]
109+
110+
if max_depth > 0:
111+
if field_data.get("type") == "object" and "fields" in field_data:
112+
sub_fields = {}
113+
for name, sub_data in field_data["fields"].items():
114+
sub_fields[name] = sub_data.get("type", "unknown")
115+
result["fields"] = sub_fields
116+
117+
if field_data.get("type") == "array" and "items" in field_data:
118+
items = field_data["items"]
119+
if items.get("type") == "object" and "fields" in items:
120+
sub_fields = {}
121+
for name, sub_data in items["fields"].items():
122+
sub_fields[name] = sub_data.get("type", "unknown")
123+
result["items"] = {"type": "object", "fields": sub_fields}
124+
else:
125+
result["items"] = {"type": items.get("type", "unknown")}
126+
127+
return result
128+
13129

14130
def _flatten_changes(old_schema: dict, new_schema: dict) -> list[dict[str, Any]]:
15131
"""İki schema arasındaki değişiklikleri düzleştirilmiş liste olarak döndür.
@@ -29,28 +145,44 @@ def _flatten_changes(old_schema: dict, new_schema: dict) -> list[dict[str, Any]]
29145
# Kaldırılan alanlar
30146
for field_name in old_fields:
31147
if field_name not in new_fields:
32-
old_type = old_fields[field_name].get("type", "unknown")
148+
old_field = old_fields[field_name]
149+
old_type = old_field.get("type", "unknown")
150+
change_type = "field_removed"
33151
change = {
34-
"type": "field_removed",
152+
"type": change_type,
153+
"change_type": _CHANGE_TYPE_LABELS.get(change_type, change_type),
35154
"path": f"$.{field_name}",
36155
"old_type": old_type,
37156
"new_type": None,
38-
"severity": get_severity("field_removed"),
39-
"message": f"Alan kaldırıldı: {field_name} ({old_type})",
157+
"old_value": _field_summary(old_field),
158+
"new_value": None,
159+
"old_preview": json.dumps(_field_to_json_preview(old_field), indent=2, ensure_ascii=False),
160+
"new_preview": None,
161+
"severity": get_severity(change_type),
162+
"message": f"Alan kaldirildi: {field_name} ({old_type})",
163+
"description": _CHANGE_TYPE_DESCRIPTIONS.get(change_type, ""),
40164
}
41165
changes.append(change)
42166

43167
# Eklenen alanlar
44168
for field_name in new_fields:
45169
if field_name not in old_fields:
46-
new_type = new_fields[field_name].get("type", "unknown")
170+
new_field = new_fields[field_name]
171+
new_type = new_field.get("type", "unknown")
172+
change_type = "field_added"
47173
change = {
48-
"type": "field_added",
174+
"type": change_type,
175+
"change_type": _CHANGE_TYPE_LABELS.get(change_type, change_type),
49176
"path": f"$.{field_name}",
50177
"old_type": None,
51178
"new_type": new_type,
52-
"severity": get_severity("field_added"),
179+
"old_value": None,
180+
"new_value": _field_summary(new_field),
181+
"old_preview": None,
182+
"new_preview": json.dumps(_field_to_json_preview(new_field), indent=2, ensure_ascii=False),
183+
"severity": get_severity(change_type),
53184
"message": f"Yeni alan: {field_name} ({new_type})",
185+
"description": _CHANGE_TYPE_DESCRIPTIONS.get(change_type, ""),
54186
}
55187
changes.append(change)
56188

@@ -64,13 +196,20 @@ def _flatten_changes(old_schema: dict, new_schema: dict) -> list[dict[str, Any]]
64196
old_type = old_field.get("type", "unknown")
65197
new_type = new_field.get("type", "unknown")
66198
if old_type != new_type:
199+
change_type = "type_changed"
67200
change = {
68-
"type": "type_changed",
201+
"type": change_type,
202+
"change_type": _CHANGE_TYPE_LABELS.get(change_type, change_type),
69203
"path": f"$.{field_name}",
70204
"old_type": old_type,
71205
"new_type": new_type,
72-
"severity": get_severity("type_changed"),
73-
"message": f"Tip değişti: {field_name} ({old_type}{new_type})",
206+
"old_value": _field_summary(old_field),
207+
"new_value": _field_summary(new_field),
208+
"old_preview": json.dumps(_field_to_json_preview(old_field), indent=2, ensure_ascii=False),
209+
"new_preview": json.dumps(_field_to_json_preview(new_field), indent=2, ensure_ascii=False),
210+
"severity": get_severity(change_type),
211+
"message": f"Tip degisti: {field_name} ({old_type} -> {new_type})",
212+
"description": _CHANGE_TYPE_DESCRIPTIONS.get(change_type, ""),
74213
}
75214
changes.append(change)
76215

@@ -84,11 +223,17 @@ def _flatten_changes(old_schema: dict, new_schema: dict) -> list[dict[str, Any]]
84223
change_type = "field_added" # nullable olması INFO seviyesinde
85224
change = {
86225
"type": change_type,
226+
"change_type": _CHANGE_TYPE_LABELS.get(change_type, change_type),
87227
"path": f"$.{field_name}",
88228
"old_type": f"nullable={old_nullable}",
89229
"new_type": f"nullable={new_nullable}",
230+
"old_value": f"nullable: {str(old_nullable).lower()}",
231+
"new_value": f"nullable: {str(new_nullable).lower()}",
232+
"old_preview": None,
233+
"new_preview": None,
90234
"severity": get_severity(change_type),
91-
"message": f"Nullable değişti: {field_name} ({old_nullable}{new_nullable})",
235+
"message": f"Nullable degisti: {field_name} ({old_nullable} -> {new_nullable})",
236+
"description": _CHANGE_TYPE_DESCRIPTIONS.get(change_type, ""),
92237
}
93238
changes.append(change)
94239

@@ -117,13 +262,20 @@ def _flatten_changes(old_schema: dict, new_schema: dict) -> list[dict[str, Any]]
117262
nested_change["severity"] = get_severity("array_item_schema_changed")
118263
changes.append(nested_change)
119264
elif old_items.get("type") != new_items.get("type"):
265+
change_type = "array_item_schema_changed"
120266
change = {
121-
"type": "array_item_schema_changed",
267+
"type": change_type,
268+
"change_type": _CHANGE_TYPE_LABELS.get(change_type, change_type),
122269
"path": f"$.{field_name}[]",
123270
"old_type": old_items.get("type", "unknown"),
124271
"new_type": new_items.get("type", "unknown"),
125-
"severity": get_severity("array_item_schema_changed"),
126-
"message": f"Array item tipi değişti: {field_name} ({old_items.get('type')}{new_items.get('type')})",
272+
"old_value": _field_summary(old_items),
273+
"new_value": _field_summary(new_items),
274+
"old_preview": json.dumps(_field_to_json_preview(old_items), indent=2, ensure_ascii=False),
275+
"new_preview": json.dumps(_field_to_json_preview(new_items), indent=2, ensure_ascii=False),
276+
"severity": get_severity(change_type),
277+
"message": f"Array item tipi degisti: {field_name} ({old_items.get('type')} -> {new_items.get('type')})",
278+
"description": _CHANGE_TYPE_DESCRIPTIONS.get(change_type, ""),
127279
}
128280
changes.append(change)
129281

0 commit comments

Comments
 (0)