refactor: split architecture rules into domain-specific files
Split the monolithic .architecture-rules.yaml (1700+ lines) into focused domain-specific files in .architecture-rules/ directory: - _main.yaml: Core config, principles, ignore patterns, severity levels - api.yaml: API endpoint rules (API-001 to API-005) - service.yaml: Service layer rules (SVC-001 to SVC-007) - model.yaml: Model rules (MDL-001 to MDL-004) - exception.yaml: Exception handling rules (EXC-001 to EXC-005) - naming.yaml: Naming convention rules (NAM-001 to NAM-005) - auth.yaml: Auth and multi-tenancy rules (AUTH-*, MT-*) - middleware.yaml: Middleware rules (MDW-001 to MDW-002) - frontend.yaml: Frontend rules (JS-*, TPL-*, FE-*, CSS-*) - language.yaml: Language/i18n rules (LANG-001 to LANG-010) - quality.yaml: Code quality rules (QUAL-001 to QUAL-003) Also creates scripts/validators/ module with base classes for future modular validator extraction. The validate_architecture.py loader now auto-detects and merges split YAML files while maintaining backward compatibility with single file mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
158
.architecture-rules/api.yaml
Normal file
158
.architecture-rules/api.yaml
Normal file
@@ -0,0 +1,158 @@
|
||||
# Architecture Rules - API Endpoint Rules
|
||||
# Rules for app/api/v1/**/*.py files
|
||||
|
||||
api_endpoint_rules:
|
||||
|
||||
- id: "API-001"
|
||||
name: "Endpoint must use Pydantic models for request/response"
|
||||
severity: "error"
|
||||
description: |
|
||||
All API endpoints must use Pydantic models (BaseModel) for request bodies
|
||||
and response models. Never use raw dicts or SQLAlchemy models directly.
|
||||
|
||||
WHY THIS MATTERS:
|
||||
- Type safety: Pydantic validates response structure at runtime
|
||||
- Documentation: OpenAPI/Swagger auto-generates accurate docs
|
||||
- Contract stability: Schema changes are explicit and reviewable
|
||||
- IDE support: Consumers get autocomplete and type hints
|
||||
- Prevents bugs: Field name mismatches caught immediately
|
||||
|
||||
COMMON VIOLATION: Service returns dict, frontend expects different field names.
|
||||
Example: Service returns {"total_imports": 5} but frontend expects {"total": 5}.
|
||||
With response_model, this mismatch is caught immediately.
|
||||
|
||||
SCHEMA LOCATION: All response schemas must be defined in models/schema/*.py,
|
||||
never inline in endpoint files. This ensures schemas are reusable and discoverable.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
anti_patterns:
|
||||
- "return dict"
|
||||
- "-> dict"
|
||||
- "return db_object"
|
||||
- "return {"
|
||||
example_good: |
|
||||
# In models/schema/stats.py
|
||||
class ImportStatsResponse(BaseModel):
|
||||
total: int
|
||||
pending: int
|
||||
completed: int
|
||||
failed: int
|
||||
|
||||
# In app/api/v1/admin/marketplace.py
|
||||
@router.get("/stats", response_model=ImportStatsResponse)
|
||||
def get_import_statistics(db: Session = Depends(get_db)):
|
||||
return stats_service.get_import_statistics(db)
|
||||
example_bad: |
|
||||
# WRONG: No response_model, returns raw dict
|
||||
@router.get("/stats")
|
||||
def get_import_statistics(db: Session = Depends(get_db)):
|
||||
return stats_service.get_import_statistics(db) # Returns dict
|
||||
|
||||
# WRONG: Schema defined inline in endpoint file
|
||||
class MyResponse(BaseModel): # Should be in models/schema/
|
||||
...
|
||||
|
||||
@router.get("/data", response_model=MyResponse)
|
||||
def get_data(): ...
|
||||
|
||||
- id: "API-002"
|
||||
name: "Endpoint must NOT contain business logic"
|
||||
severity: "error"
|
||||
description: |
|
||||
API endpoints should only handle HTTP concerns (validation, auth, response formatting).
|
||||
All business logic must be delegated to service layer.
|
||||
|
||||
Transaction control (db.commit) IS allowed at endpoint level - this is the recommended
|
||||
pattern for request-scoped transactions. One request = one transaction.
|
||||
|
||||
What's NOT allowed:
|
||||
- db.add() - creating entities is business logic
|
||||
- db.query() - complex queries are business logic
|
||||
- db.delete() - deleting entities is business logic
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
anti_patterns:
|
||||
- "db.add("
|
||||
- "db.delete("
|
||||
- "db.query("
|
||||
|
||||
- id: "API-003"
|
||||
name: "Endpoint must NOT raise ANY exceptions directly"
|
||||
severity: "error"
|
||||
description: |
|
||||
API endpoints should NOT raise exceptions directly. Endpoints are a thin
|
||||
orchestration layer that:
|
||||
1. Accepts request parameters (validated by Pydantic)
|
||||
2. Calls dependencies for authentication/authorization (deps.py raises exceptions)
|
||||
3. Calls services for business logic (services raise domain exceptions)
|
||||
4. Returns response (formatted by Pydantic)
|
||||
|
||||
Exception raising belongs in:
|
||||
- Dependencies (app/api/deps.py) - authentication/authorization validation
|
||||
- Services (app/services/) - business logic validation
|
||||
|
||||
The global exception handler catches all WizamartException subclasses and
|
||||
converts them to appropriate HTTP responses.
|
||||
|
||||
WRONG (endpoint raises exception):
|
||||
@router.get("/orders")
|
||||
def get_orders(current_user: User = Depends(get_current_vendor_api)):
|
||||
if not hasattr(current_user, "token_vendor_id"): # Redundant check
|
||||
raise InvalidTokenException("...") # Don't raise here
|
||||
return order_service.get_orders(db, current_user.token_vendor_id)
|
||||
|
||||
RIGHT (dependency guarantees, endpoint trusts):
|
||||
@router.get("/orders")
|
||||
def get_orders(current_user: User = Depends(get_current_vendor_api)):
|
||||
# Dependency guarantees token_vendor_id is present
|
||||
return order_service.get_orders(db, current_user.token_vendor_id)
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
anti_patterns:
|
||||
- "raise HTTPException"
|
||||
- "raise InvalidTokenException"
|
||||
- "raise InsufficientPermissionsException"
|
||||
- "if not hasattr\\(current_user.*token_vendor"
|
||||
exceptions:
|
||||
- "app/exceptions/handler.py"
|
||||
|
||||
- id: "API-004"
|
||||
name: "Endpoint must have proper authentication/authorization"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Protected endpoints must use Depends() for authentication.
|
||||
Use get_current_user, get_current_admin, etc.
|
||||
|
||||
Auto-excluded files:
|
||||
- */auth.py - Authentication endpoints (login, logout, register) are intentionally public
|
||||
|
||||
Public endpoint markers (place on line before or after decorator):
|
||||
- # public - Descriptive marker for intentionally unauthenticated endpoints
|
||||
- # noqa: API-004 - Standard noqa style to suppress warning
|
||||
|
||||
Example:
|
||||
# public - Stripe webhook receives external callbacks
|
||||
@router.post("/webhook/stripe")
|
||||
def stripe_webhook(request: Request):
|
||||
...
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
required_if_not_public:
|
||||
- "Depends(get_current_"
|
||||
auto_exclude_files:
|
||||
- "*/auth.py"
|
||||
public_markers:
|
||||
- "# public"
|
||||
- "# noqa: api-004"
|
||||
|
||||
- id: "API-005"
|
||||
name: "Multi-tenant endpoints must scope queries to vendor_id"
|
||||
severity: "error"
|
||||
description: |
|
||||
All queries in vendor/shop contexts must filter by vendor_id.
|
||||
Use request.state.vendor_id from middleware.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/vendor/**/*.py"
|
||||
file_pattern: "app/api/v1/shop/**/*.py"
|
||||
discouraged_patterns:
|
||||
- "db.query(.*).all()"
|
||||
Reference in New Issue
Block a user