Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Batch align error responses in GoChat handlers to Chatwoot's error code convention.
|
||||
|
||||
Chatwoot conventions:
|
||||
- Record not found → 404
|
||||
- Validation/create/update/delete failures → 422 UnprocessableEntity
|
||||
- Auth failures → 401 Unauthorized
|
||||
- Permission failures → 403 Forbidden
|
||||
- Request format/bind errors → 400 BadRequest
|
||||
- Conflict/duplicate → 409 Conflict
|
||||
- Internal server errors should be rare (only for unexpected DB/infra failures)
|
||||
|
||||
Current GoChat pattern: most errors use 500 InternalServerError for business logic failures.
|
||||
Target: replace 500 with appropriate 422/404/401/403 based on error message content.
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Error message → target HTTP status mapping
|
||||
# Pattern: if the gin.H{"error": "..."} message matches a keyword, use the mapped status
|
||||
MESSAGE_STATUS_MAP = {
|
||||
# 404 patterns
|
||||
'not found': 'http.StatusNotFound',
|
||||
'does not exist': 'http.StatusNotFound',
|
||||
|
||||
# 401 patterns
|
||||
'unauthorized': 'http.StatusUnauthorized',
|
||||
'authentication': 'http.StatusUnauthorized',
|
||||
'invalid token': 'http.StatusUnauthorized',
|
||||
|
||||
# 403 patterns
|
||||
'forbidden': 'http.StatusForbidden',
|
||||
'not allowed': 'http.StatusForbidden',
|
||||
'permission': 'http.StatusForbidden',
|
||||
'access denied': 'http.StatusForbidden',
|
||||
'does not belong': 'http.StatusForbidden', # "does not belong to this account"
|
||||
|
||||
# 409 patterns
|
||||
'already exists': 'http.StatusConflict',
|
||||
'duplicate': 'http.StatusConflict',
|
||||
|
||||
# 422 patterns (default for business logic failures)
|
||||
'failed to': 'http.StatusUnprocessableEntity', # "failed to create/update/delete/list"
|
||||
'invalid': 'http.StatusUnprocessableEntity',
|
||||
'cannot': 'http.StatusUnprocessableEntity',
|
||||
'must': 'http.StatusUnprocessableEntity',
|
||||
'required': 'http.StatusUnprocessableEntity',
|
||||
'missing': 'http.StatusUnprocessableEntity',
|
||||
}
|
||||
|
||||
def classify_error_status(error_msg: str) -> str:
|
||||
"""Classify an error message into the appropriate HTTP status code constant."""
|
||||
lower_msg = error_msg.lower()
|
||||
|
||||
# Check in priority order — specific patterns first
|
||||
for pattern, status in MESSAGE_STATUS_MAP.items():
|
||||
if pattern in lower_msg:
|
||||
return status
|
||||
|
||||
# Default: 422 for business logic failures that were previously 500
|
||||
return 'http.StatusUnprocessableEntity'
|
||||
|
||||
def process_file(filepath: str) -> dict:
|
||||
"""Process a single Go file, replacing 500 errors with appropriate status codes."""
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
changes = []
|
||||
|
||||
# Pattern: c.JSON(http.StatusInternalServerError, gin.H{"error": "..."})
|
||||
# Replace with appropriate status code based on error message
|
||||
pattern = re.compile(
|
||||
r'c\.JSON\(http\.StatusInternalServerError,\s*gin\.H\{'
|
||||
r'"error":\s*"([^"]+)"'
|
||||
r'\}\)'
|
||||
)
|
||||
|
||||
def replacer(match):
|
||||
error_msg = match.group(1)
|
||||
new_status = classify_error_status(error_msg)
|
||||
old = match.group(0)
|
||||
new = f'c.JSON({new_status}, gin.H{{"error": "{error_msg}"}})'
|
||||
changes.append({
|
||||
'line': content[:match.start()].count('\n') + 1,
|
||||
'old': old,
|
||||
'new': new,
|
||||
'status': new_status.replace('http.Status', ''),
|
||||
})
|
||||
return new
|
||||
|
||||
content = pattern.sub(replacer, content)
|
||||
|
||||
# Also handle: c.JSON(http.StatusInternalServerError, response.APIResponse{...})
|
||||
pattern2 = re.compile(
|
||||
r'c\.JSON\(http\.StatusInternalServerError,\s*response\.APIResponse\{[^}]+\}\)'
|
||||
)
|
||||
# For these, just replace status code — can't easily classify the message
|
||||
content2 = re.sub(
|
||||
r'c\.JSON\(http\.StatusInternalServerError,\s*response\.APIResponse',
|
||||
'c.JSON(http.StatusUnprocessableEntity, response.APIResponse',
|
||||
content
|
||||
)
|
||||
if content2 != content:
|
||||
changes.append({'line': 0, 'old': '500 APIResponse', 'new': '422 APIResponse', 'status': 'UnprocessableEntity'})
|
||||
content = content2
|
||||
|
||||
if content != original_content:
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return {'file': filepath, 'changes': changes, 'total_changes': len(changes)}
|
||||
|
||||
def main():
|
||||
handler_dir = '/home/yanghao05/Workspace/gochat/internal/handler'
|
||||
|
||||
# Find all handler Go files (exclude tests)
|
||||
go_files = []
|
||||
for root, dirs, files in os.walk(handler_dir):
|
||||
for f in files:
|
||||
if f.endswith('.go') and not f.endswith('_test.go'):
|
||||
go_files.append(os.path.join(root, f))
|
||||
|
||||
total_changes = 0
|
||||
status_distribution = {}
|
||||
|
||||
for filepath in go_files:
|
||||
result = process_file(filepath)
|
||||
if result['total_changes'] > 0:
|
||||
total_changes += result['total_changes']
|
||||
for change in result['changes']:
|
||||
status = change['status']
|
||||
status_distribution[status] = status_distribution.get(status, 0) + 1
|
||||
print(f" {filepath}:{change['line']} → {status}: {change['new']}")
|
||||
|
||||
print(f"\n=== Summary ===")
|
||||
print(f"Files processed: {len(go_files)}")
|
||||
print(f"Total changes: {total_changes}")
|
||||
print(f"Status distribution:")
|
||||
for status, count in sorted(status_distribution.items()):
|
||||
print(f" {status}: {count}")
|
||||
|
||||
# Also handle 400 → 422 for "failed to" business logic errors
|
||||
# Pattern: c.JSON(http.StatusBadRequest, gin.H{"error": "failed to..."})
|
||||
print(f"\n=== Phase 2: 400 → 422 for business logic errors ===")
|
||||
|
||||
for filepath in go_files:
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
original = content
|
||||
|
||||
# Replace 400 with 422 for "failed to" errors (business logic, not request format)
|
||||
pattern = re.compile(
|
||||
r'c\.JSON\(http\.StatusBadRequest,\s*gin\.H\{'
|
||||
r'"error":\s*"([^"]+)"'
|
||||
r'\}\)'
|
||||
)
|
||||
|
||||
changes_400 = []
|
||||
def replacer400(match):
|
||||
error_msg = match.group(1)
|
||||
lower = error_msg.lower()
|
||||
# Only replace if it's a business logic failure, not a request format error
|
||||
if 'failed to' in lower or 'cannot' in lower or 'must' in lower:
|
||||
new = f'c.JSON(http.StatusUnprocessableEntity, gin.H{{"error": "{error_msg}"}})'
|
||||
changes_400.append({'msg': error_msg, 'status': 'UnprocessableEntity'})
|
||||
return new
|
||||
return match.group(0)
|
||||
|
||||
content = pattern.sub(replacer400, content)
|
||||
|
||||
if content != original:
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
for c in changes_400:
|
||||
print(f" {filepath}: 400→422: {c['msg']}")
|
||||
total_changes += len(changes_400)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user