1887 lines
43 KiB
Markdown
1887 lines
43 KiB
Markdown
|
|
# ORIGIN_CODE_STANDARDS.md
|
||
|
|
|
||
|
|
## 📋 RÉSUMÉ EXÉCUTIF
|
||
|
|
|
||
|
|
Ce document définit les standards de code complets et définitifs pour la plateforme Veza. Il couvre les conventions de code, architectures, patterns, et anti-patterns pour Go (backend), Rust (services temps réel), TypeScript/React (frontend), CSS/Tailwind, Git, et documentation. Ces standards garantissent la maintenabilité, lisibilité, performance, et cohérence du code pendant 24 mois.
|
||
|
|
|
||
|
|
## 🎯 OBJECTIFS
|
||
|
|
|
||
|
|
### Objectif Principal
|
||
|
|
Établir des standards de code stricts et immuables qui garantissent la qualité, maintenabilité, et cohérence du codebase pendant 24 mois avec une équipe de 10+ développeurs.
|
||
|
|
|
||
|
|
### Objectifs Secondaires
|
||
|
|
- Réduire la dette technique (< 5% du temps de développement)
|
||
|
|
- Faciliter l'onboarding (< 1 semaine pour nouveaux développeurs)
|
||
|
|
- Garantir la lisibilité (code self-documenting)
|
||
|
|
- Optimiser les performances (hot paths identifiés)
|
||
|
|
- Standardiser le style (linters, formatters automatiques)
|
||
|
|
|
||
|
|
## 📖 TABLE DES MATIÈRES
|
||
|
|
|
||
|
|
1. [General Principles](#1-general-principles)
|
||
|
|
2. [Go Standards (Backend)](#2-go-standards-backend)
|
||
|
|
3. [Rust Standards (Services)](#3-rust-standards-services)
|
||
|
|
4. [TypeScript Standards (Frontend)](#4-typescript-standards-frontend)
|
||
|
|
5. [React Standards](#5-react-standards)
|
||
|
|
6. [CSS/Tailwind Standards](#6-csstailwind-standards)
|
||
|
|
7. [Git Standards](#7-git-standards)
|
||
|
|
8. [Documentation Standards](#8-documentation-standards)
|
||
|
|
9. [Code Review Process](#9-code-review-process)
|
||
|
|
10. [Refactoring Guidelines](#10-refactoring-guidelines)
|
||
|
|
11. [Anti-Patterns Library](#11-anti-patterns-library)
|
||
|
|
|
||
|
|
## 🔒 RÈGLES IMMUABLES
|
||
|
|
|
||
|
|
1. **Formatters OBLIGATOIRES**: gofmt (Go), rustfmt (Rust), Prettier (TS/React)
|
||
|
|
2. **Linters OBLIGATOIRES**: golangci-lint (Go), clippy (Rust), ESLint (TS)
|
||
|
|
3. **Tests OBLIGATOIRES** pour toute nouvelle feature (coverage ≥ 80%)
|
||
|
|
4. **Code review OBLIGATOIRE** (2 approbations minimum)
|
||
|
|
5. **Naming conventions STRICTES** (camelCase/PascalCase/snake_case selon langage)
|
||
|
|
6. **Documentation OBLIGATOIRE** pour fonctions publiques
|
||
|
|
7. **Error handling COMPLET** (pas de panic/unwrap en production)
|
||
|
|
8. **Magic numbers INTERDITS** (utiliser constantes nommées)
|
||
|
|
9. **Code mort INTERDIT** (suppression immédiate)
|
||
|
|
10. **Complexity limit**: Fonctions max 50 lignes, cyclomatic complexity < 10
|
||
|
|
|
||
|
|
## 1. GENERAL PRINCIPLES
|
||
|
|
|
||
|
|
### 1.1 SOLID Principles
|
||
|
|
|
||
|
|
**Single Responsibility Principle** (SRP):
|
||
|
|
```go
|
||
|
|
// ❌ Bad: UserService does too much
|
||
|
|
type UserService struct{}
|
||
|
|
func (s *UserService) CreateUser() {}
|
||
|
|
func (s *UserService) SendEmail() {}
|
||
|
|
func (s *UserService) ProcessPayment() {}
|
||
|
|
|
||
|
|
// ✅ Good: Separate services
|
||
|
|
type UserService struct{}
|
||
|
|
func (s *UserService) CreateUser() {}
|
||
|
|
|
||
|
|
type EmailService struct{}
|
||
|
|
func (s *EmailService) SendEmail() {}
|
||
|
|
|
||
|
|
type PaymentService struct{}
|
||
|
|
func (s *PaymentService) ProcessPayment() {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Open/Closed Principle** (OCP):
|
||
|
|
```go
|
||
|
|
// ✅ Good: Open for extension, closed for modification
|
||
|
|
type NotificationSender interface {
|
||
|
|
Send(message string) error
|
||
|
|
}
|
||
|
|
|
||
|
|
type EmailNotification struct{}
|
||
|
|
func (e *EmailNotification) Send(message string) error { /* ... */ }
|
||
|
|
|
||
|
|
type SMSNotification struct{}
|
||
|
|
func (s *SMSNotification) Send(message string) error { /* ... */ }
|
||
|
|
|
||
|
|
// Add new notification types without modifying existing code
|
||
|
|
type PushNotification struct{}
|
||
|
|
func (p *PushNotification) Send(message string) error { /* ... */ }
|
||
|
|
```
|
||
|
|
|
||
|
|
**Liskov Substitution Principle** (LSP):
|
||
|
|
```go
|
||
|
|
// ✅ Good: Subtypes can replace base types
|
||
|
|
type Storage interface {
|
||
|
|
Save(data []byte) error
|
||
|
|
Load() ([]byte, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type S3Storage struct{}
|
||
|
|
func (s *S3Storage) Save(data []byte) error { /* ... */ }
|
||
|
|
func (s *S3Storage) Load() ([]byte, error) { /* ... */ }
|
||
|
|
|
||
|
|
type LocalStorage struct{}
|
||
|
|
func (l *LocalStorage) Save(data []byte) error { /* ... */ }
|
||
|
|
func (l *LocalStorage) Load() ([]byte, error) { /* ... */ }
|
||
|
|
|
||
|
|
// Can swap implementations
|
||
|
|
var storage Storage = &S3Storage{} // or &LocalStorage{}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Interface Segregation Principle** (ISP):
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Fat interface
|
||
|
|
type Worker interface {
|
||
|
|
Work()
|
||
|
|
Eat()
|
||
|
|
Sleep()
|
||
|
|
Code()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Segregated interfaces
|
||
|
|
type Workable interface {
|
||
|
|
Work()
|
||
|
|
}
|
||
|
|
|
||
|
|
type Eatable interface {
|
||
|
|
Eat()
|
||
|
|
}
|
||
|
|
|
||
|
|
type Sleepable interface {
|
||
|
|
Sleep()
|
||
|
|
}
|
||
|
|
|
||
|
|
type Codeable interface {
|
||
|
|
Code()
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Dependency Inversion Principle** (DIP):
|
||
|
|
```go
|
||
|
|
// ❌ Bad: High-level module depends on low-level module
|
||
|
|
type UserService struct {
|
||
|
|
repo *PostgresUserRepository // Concrete dependency
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Both depend on abstraction
|
||
|
|
type UserRepository interface {
|
||
|
|
Create(user *User) error
|
||
|
|
FindByID(id uuid.UUID) (*User, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type UserService struct {
|
||
|
|
repo UserRepository // Abstract dependency
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implementations
|
||
|
|
type PostgresUserRepository struct{}
|
||
|
|
type MongoUserRepository struct{}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.2 DRY (Don't Repeat Yourself)
|
||
|
|
|
||
|
|
**Extract Common Logic**:
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Duplication
|
||
|
|
func CreateUser(req CreateUserRequest) error {
|
||
|
|
if req.Email == "" {
|
||
|
|
return errors.New("email required")
|
||
|
|
}
|
||
|
|
if !isValidEmail(req.Email) {
|
||
|
|
return errors.New("invalid email")
|
||
|
|
}
|
||
|
|
// ... create user
|
||
|
|
}
|
||
|
|
|
||
|
|
func UpdateUser(req UpdateUserRequest) error {
|
||
|
|
if req.Email == "" {
|
||
|
|
return errors.New("email required")
|
||
|
|
}
|
||
|
|
if !isValidEmail(req.Email) {
|
||
|
|
return errors.New("invalid email")
|
||
|
|
}
|
||
|
|
// ... update user
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Extract validation
|
||
|
|
func validateEmail(email string) error {
|
||
|
|
if email == "" {
|
||
|
|
return errors.New("email required")
|
||
|
|
}
|
||
|
|
if !isValidEmail(email) {
|
||
|
|
return errors.New("invalid email")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func CreateUser(req CreateUserRequest) error {
|
||
|
|
if err := validateEmail(req.Email); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
// ... create user
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 KISS (Keep It Simple, Stupid)
|
||
|
|
|
||
|
|
**Favor Simplicity**:
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Overengineered
|
||
|
|
func IsEven(n int) bool {
|
||
|
|
return n & 1 == 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Simple and readable
|
||
|
|
func IsEven(n int) bool {
|
||
|
|
return n % 2 == 0
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.4 YAGNI (You Aren't Gonna Need It)
|
||
|
|
|
||
|
|
**Don't Add Features Until Needed**:
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Premature generalization
|
||
|
|
type Cache interface {
|
||
|
|
Get(key string) (interface{}, error)
|
||
|
|
Set(key string, value interface{}) error
|
||
|
|
Delete(key string) error
|
||
|
|
GetMulti(keys []string) ([]interface{}, error) // Not needed yet
|
||
|
|
SetMulti(map[string]interface{}) error // Not needed yet
|
||
|
|
Flush() error // Not needed yet
|
||
|
|
GetStats() CacheStats // Not needed yet
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Start simple, add when needed
|
||
|
|
type Cache interface {
|
||
|
|
Get(key string) (interface{}, error)
|
||
|
|
Set(key string, value interface{}) error
|
||
|
|
Delete(key string) error
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 2. GO STANDARDS (BACKEND)
|
||
|
|
|
||
|
|
### 2.1 Project Structure (Clean Architecture)
|
||
|
|
|
||
|
|
```
|
||
|
|
veza-backend-api/
|
||
|
|
├── cmd/
|
||
|
|
│ └── api/
|
||
|
|
│ └── main.go # Entry point
|
||
|
|
├── internal/
|
||
|
|
│ ├── api/ # HTTP handlers (Delivery layer)
|
||
|
|
│ │ └── handlers/
|
||
|
|
│ │ ├── user_handlers.go
|
||
|
|
│ │ ├── track_handlers.go
|
||
|
|
│ │ └── auth_handlers.go
|
||
|
|
│ ├── core/ # Business logic (Use Cases)
|
||
|
|
│ │ ├── services/
|
||
|
|
│ │ │ ├── user_service.go
|
||
|
|
│ │ │ └── track_service.go
|
||
|
|
│ │ └── domain/ # Entities
|
||
|
|
│ │ ├── user.go
|
||
|
|
│ │ └── track.go
|
||
|
|
│ ├── repository/ # Data access (Repository pattern)
|
||
|
|
│ │ ├── interfaces.go
|
||
|
|
│ │ ├── user_repository.go
|
||
|
|
│ │ └── track_repository.go
|
||
|
|
│ ├── infrastructure/ # External dependencies
|
||
|
|
│ │ ├── database/
|
||
|
|
│ │ │ └── postgres.go
|
||
|
|
│ │ ├── cache/
|
||
|
|
│ │ │ └── redis.go
|
||
|
|
│ │ └── storage/
|
||
|
|
│ │ └── s3.go
|
||
|
|
│ ├── middleware/ # HTTP middleware
|
||
|
|
│ │ ├── auth.go
|
||
|
|
│ │ ├── cors.go
|
||
|
|
│ │ └── logging.go
|
||
|
|
│ └── config/ # Configuration
|
||
|
|
│ └── config.go
|
||
|
|
├── pkg/ # Public packages (reusable)
|
||
|
|
│ ├── logger/
|
||
|
|
│ └── validator/
|
||
|
|
├── migrations/ # Database migrations
|
||
|
|
├── tests/
|
||
|
|
│ ├── unit/
|
||
|
|
│ └── integration/
|
||
|
|
├── go.mod
|
||
|
|
└── go.sum
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 Naming Conventions
|
||
|
|
|
||
|
|
**Variables**: camelCase
|
||
|
|
```go
|
||
|
|
var userName string
|
||
|
|
var userCount int
|
||
|
|
```
|
||
|
|
|
||
|
|
**Constants**: PascalCase or SCREAMING_SNAKE_CASE (for exported)
|
||
|
|
```go
|
||
|
|
const MaxRetries = 3
|
||
|
|
const DEFAULT_TIMEOUT = 30 * time.Second
|
||
|
|
```
|
||
|
|
|
||
|
|
**Functions**: PascalCase (exported), camelCase (unexported)
|
||
|
|
```go
|
||
|
|
// Exported
|
||
|
|
func CreateUser(req *CreateUserRequest) (*User, error) {}
|
||
|
|
|
||
|
|
// Unexported
|
||
|
|
func validateEmail(email string) error {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Types**: PascalCase
|
||
|
|
```go
|
||
|
|
type UserService struct {}
|
||
|
|
type CreateUserRequest struct {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Interfaces**: -er suffix (if single method)
|
||
|
|
```go
|
||
|
|
type Reader interface {
|
||
|
|
Read(p []byte) (n int, err error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type UserRepository interface { // Multi-method, no -er suffix
|
||
|
|
Create(user *User) error
|
||
|
|
FindByID(id uuid.UUID) (*User, error)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.3 Error Handling
|
||
|
|
|
||
|
|
**Always Check Errors**:
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Ignoring error
|
||
|
|
user, _ := repo.FindByID(id)
|
||
|
|
|
||
|
|
// ✅ Good: Check and handle
|
||
|
|
user, err := repo.FindByID(id)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to find user: %w", err)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Wrap Errors with Context**:
|
||
|
|
```go
|
||
|
|
import "fmt"
|
||
|
|
|
||
|
|
func CreateUser(req *CreateUserRequest) error {
|
||
|
|
if err := validateEmail(req.Email); err != nil {
|
||
|
|
return fmt.Errorf("validation failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := repo.Create(user); err != nil {
|
||
|
|
return fmt.Errorf("failed to create user in database: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Custom Error Types** (when needed):
|
||
|
|
```go
|
||
|
|
type ValidationError struct {
|
||
|
|
Field string
|
||
|
|
Message string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (e *ValidationError) Error() string {
|
||
|
|
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
if req.Email == "" {
|
||
|
|
return &ValidationError{Field: "email", Message: "required"}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Don't Panic in Production**:
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Panic for recoverable errors
|
||
|
|
if err != nil {
|
||
|
|
panic(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Return error
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("operation failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ⚠️ OK: Panic only for programmer errors (init, config)
|
||
|
|
func init() {
|
||
|
|
if os.Getenv("DATABASE_URL") == "" {
|
||
|
|
panic("DATABASE_URL environment variable not set")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.4 Function Design
|
||
|
|
|
||
|
|
**Keep Functions Small** (< 50 lines):
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Too long
|
||
|
|
func CreateUser(c *gin.Context) {
|
||
|
|
var req CreateUserRequest
|
||
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||
|
|
c.JSON(400, gin.H{"error": "invalid request"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if req.Email == "" {
|
||
|
|
c.JSON(400, gin.H{"error": "email required"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if !isValidEmail(req.Email) {
|
||
|
|
c.JSON(400, gin.H{"error": "invalid email"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
existingUser, _ := repo.FindByEmail(req.Email)
|
||
|
|
if existingUser != nil {
|
||
|
|
c.JSON(409, gin.H{"error": "email already exists"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
|
||
|
|
if err != nil {
|
||
|
|
c.JSON(500, gin.H{"error": "internal error"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
user := &User{
|
||
|
|
ID: uuid.New(),
|
||
|
|
Email: req.Email,
|
||
|
|
PasswordHash: string(hash),
|
||
|
|
CreatedAt: time.Now(),
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := repo.Create(user); err != nil {
|
||
|
|
c.JSON(500, gin.H{"error": "failed to create user"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
c.JSON(201, user)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Extract to service layer
|
||
|
|
func CreateUser(c *gin.Context) {
|
||
|
|
var req CreateUserRequest
|
||
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||
|
|
c.JSON(400, ErrorResponse{Code: 2000, Message: "Invalid request"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
user, err := userService.CreateUser(c.Request.Context(), &req)
|
||
|
|
if err != nil {
|
||
|
|
handleError(c, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
c.JSON(201, user)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Business logic in service
|
||
|
|
func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
|
||
|
|
if err := s.validateCreateRequest(req); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := s.checkEmailExists(req.Email); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
user := s.buildUser(req)
|
||
|
|
|
||
|
|
if err := s.repo.Create(ctx, user); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return user, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Single Return Type** (prefer):
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Multiple return patterns
|
||
|
|
func FindUser(id uuid.UUID) (*User, error) {
|
||
|
|
user, err := repo.FindByID(id)
|
||
|
|
if err == sql.ErrNoRows {
|
||
|
|
return nil, nil // nil user, nil error
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err // nil user, error
|
||
|
|
}
|
||
|
|
return user, nil // user, nil error
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Consistent return pattern
|
||
|
|
func FindUser(id uuid.UUID) (*User, error) {
|
||
|
|
user, err := repo.FindByID(id)
|
||
|
|
if err == sql.ErrNoRows {
|
||
|
|
return nil, ErrUserNotFound
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to find user: %w", err)
|
||
|
|
}
|
||
|
|
return user, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.5 Concurrency
|
||
|
|
|
||
|
|
**Use Context for Cancellation**:
|
||
|
|
```go
|
||
|
|
func ProcessTask(ctx context.Context, taskID uuid.UUID) error {
|
||
|
|
select {
|
||
|
|
case <-ctx.Done():
|
||
|
|
return ctx.Err() // Cancelled or timed out
|
||
|
|
case <-time.After(1 * time.Second):
|
||
|
|
// Continue processing
|
||
|
|
}
|
||
|
|
|
||
|
|
// ... do work
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage with timeout
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
if err := ProcessTask(ctx, taskID); err != nil {
|
||
|
|
log.Error("Task failed", zap.Error(err))
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Goroutine Patterns**:
|
||
|
|
```go
|
||
|
|
// ✅ Good: Worker pool
|
||
|
|
func ProcessTracks(tracks []*Track) error {
|
||
|
|
const numWorkers = 10
|
||
|
|
jobs := make(chan *Track, len(tracks))
|
||
|
|
results := make(chan error, len(tracks))
|
||
|
|
|
||
|
|
// Start workers
|
||
|
|
for w := 0; w < numWorkers; w++ {
|
||
|
|
go func() {
|
||
|
|
for track := range jobs {
|
||
|
|
results <- processTrack(track)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Send jobs
|
||
|
|
for _, track := range tracks {
|
||
|
|
jobs <- track
|
||
|
|
}
|
||
|
|
close(jobs)
|
||
|
|
|
||
|
|
// Collect results
|
||
|
|
for i := 0; i < len(tracks); i++ {
|
||
|
|
if err := <-results; err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Avoid Goroutine Leaks**:
|
||
|
|
```go
|
||
|
|
// ❌ Bad: Goroutine leak (no way to stop)
|
||
|
|
func StartWorker() {
|
||
|
|
go func() {
|
||
|
|
for {
|
||
|
|
doWork()
|
||
|
|
time.Sleep(1 * time.Second)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Cancellable goroutine
|
||
|
|
func StartWorker(ctx context.Context) {
|
||
|
|
go func() {
|
||
|
|
ticker := time.NewTicker(1 * time.Second)
|
||
|
|
defer ticker.Stop()
|
||
|
|
|
||
|
|
for {
|
||
|
|
select {
|
||
|
|
case <-ctx.Done():
|
||
|
|
return // Exit goroutine
|
||
|
|
case <-ticker.C:
|
||
|
|
doWork()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.6 Testing
|
||
|
|
|
||
|
|
**Table-Driven Tests**:
|
||
|
|
```go
|
||
|
|
func TestIsValidEmail(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
email string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{"valid email", "user@example.com", true},
|
||
|
|
{"missing @", "userexample.com", false},
|
||
|
|
{"missing domain", "user@", false},
|
||
|
|
{"empty", "", false},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
got := IsValidEmail(tt.email)
|
||
|
|
if got != tt.want {
|
||
|
|
t.Errorf("IsValidEmail(%q) = %v, want %v", tt.email, got, tt.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Use testify/assert**:
|
||
|
|
```go
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestCreateUser(t *testing.T) {
|
||
|
|
user, err := CreateUser(&CreateUserRequest{
|
||
|
|
Email: "test@example.com",
|
||
|
|
})
|
||
|
|
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, user)
|
||
|
|
assert.Equal(t, "test@example.com", user.Email)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Mock Dependencies**:
|
||
|
|
```go
|
||
|
|
// Interface
|
||
|
|
type UserRepository interface {
|
||
|
|
Create(user *User) error
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mock implementation (using testify/mock)
|
||
|
|
type MockUserRepository struct {
|
||
|
|
mock.Mock
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *MockUserRepository) Create(user *User) error {
|
||
|
|
args := m.Called(user)
|
||
|
|
return args.Error(0)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test with mock
|
||
|
|
func TestUserService_CreateUser(t *testing.T) {
|
||
|
|
mockRepo := new(MockUserRepository)
|
||
|
|
mockRepo.On("Create", mock.Anything).Return(nil)
|
||
|
|
|
||
|
|
service := NewUserService(mockRepo)
|
||
|
|
user, err := service.CreateUser(&CreateUserRequest{
|
||
|
|
Email: "test@example.com",
|
||
|
|
})
|
||
|
|
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, user)
|
||
|
|
mockRepo.AssertExpectations(t)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 3. RUST STANDARDS (SERVICES)
|
||
|
|
|
||
|
|
### 3.1 Project Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
veza-chat-server/
|
||
|
|
├── src/
|
||
|
|
│ ├── main.rs # Entry point
|
||
|
|
│ ├── lib.rs # Library root
|
||
|
|
│ ├── handlers/ # WebSocket handlers
|
||
|
|
│ │ ├── mod.rs
|
||
|
|
│ │ ├── message.rs
|
||
|
|
│ │ └── presence.rs
|
||
|
|
│ ├── services/ # Business logic
|
||
|
|
│ │ ├── mod.rs
|
||
|
|
│ │ ├── message_service.rs
|
||
|
|
│ │ └── auth_service.rs
|
||
|
|
│ ├── repository/ # Data access
|
||
|
|
│ │ ├── mod.rs
|
||
|
|
│ │ └── message_repository.rs
|
||
|
|
│ ├── models/ # Data models
|
||
|
|
│ │ ├── mod.rs
|
||
|
|
│ │ ├── message.rs
|
||
|
|
│ │ └── room.rs
|
||
|
|
│ ├── utils/ # Utilities
|
||
|
|
│ │ ├── mod.rs
|
||
|
|
│ │ └── jwt.rs
|
||
|
|
│ └── config.rs # Configuration
|
||
|
|
├── migrations/ # SQLx migrations
|
||
|
|
├── tests/
|
||
|
|
│ ├── integration/
|
||
|
|
│ └── unit/
|
||
|
|
├── Cargo.toml
|
||
|
|
└── Cargo.lock
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 Naming Conventions
|
||
|
|
|
||
|
|
**Variables/Functions**: snake_case
|
||
|
|
```rust
|
||
|
|
let user_name = "John";
|
||
|
|
fn create_user() {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Types/Traits**: PascalCase
|
||
|
|
```rust
|
||
|
|
struct UserService;
|
||
|
|
trait Repository {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Constants**: SCREAMING_SNAKE_CASE
|
||
|
|
```rust
|
||
|
|
const MAX_CONNECTIONS: usize = 10000;
|
||
|
|
```
|
||
|
|
|
||
|
|
**Lifetimes**: short, lowercase
|
||
|
|
```rust
|
||
|
|
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 Error Handling
|
||
|
|
|
||
|
|
**Use Result Type**:
|
||
|
|
```rust
|
||
|
|
use anyhow::{Result, Context};
|
||
|
|
|
||
|
|
fn create_user(email: &str) -> Result<User> {
|
||
|
|
if email.is_empty() {
|
||
|
|
return Err(anyhow!("Email is required"));
|
||
|
|
}
|
||
|
|
|
||
|
|
let user = User::new(email)
|
||
|
|
.context("Failed to create user")?;
|
||
|
|
|
||
|
|
Ok(user)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Custom Error Types** (with thiserror):
|
||
|
|
```rust
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum UserError {
|
||
|
|
#[error("User not found: {0}")]
|
||
|
|
NotFound(Uuid),
|
||
|
|
|
||
|
|
#[error("Email already exists: {0}")]
|
||
|
|
EmailExists(String),
|
||
|
|
|
||
|
|
#[error("Validation failed: {0}")]
|
||
|
|
Validation(String),
|
||
|
|
|
||
|
|
#[error(transparent)]
|
||
|
|
Database(#[from] sqlx::Error),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
fn find_user(id: Uuid) -> Result<User, UserError> {
|
||
|
|
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
||
|
|
.fetch_one(&pool)
|
||
|
|
.await
|
||
|
|
.map_err(|_| UserError::NotFound(id))?;
|
||
|
|
|
||
|
|
Ok(user)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Avoid unwrap/expect in Production**:
|
||
|
|
```rust
|
||
|
|
// ❌ Bad: unwrap can panic
|
||
|
|
let user = find_user(id).unwrap();
|
||
|
|
|
||
|
|
// ✅ Good: Handle error
|
||
|
|
let user = find_user(id)
|
||
|
|
.context("Failed to find user")?;
|
||
|
|
|
||
|
|
// ⚠️ OK: expect for programmer errors (init)
|
||
|
|
let config = Config::from_env()
|
||
|
|
.expect("CONFIG environment variables missing");
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.4 Ownership & Borrowing
|
||
|
|
|
||
|
|
**Prefer Borrowing**:
|
||
|
|
```rust
|
||
|
|
// ❌ Bad: Takes ownership (can't use afterwards)
|
||
|
|
fn process_user(user: User) {
|
||
|
|
println!("{}", user.name);
|
||
|
|
}
|
||
|
|
|
||
|
|
let user = User::new("John");
|
||
|
|
process_user(user);
|
||
|
|
// user is moved, can't use here
|
||
|
|
|
||
|
|
// ✅ Good: Borrows (can still use)
|
||
|
|
fn process_user(user: &User) {
|
||
|
|
println!("{}", user.name);
|
||
|
|
}
|
||
|
|
|
||
|
|
let user = User::new("John");
|
||
|
|
process_user(&user);
|
||
|
|
// user still usable here
|
||
|
|
```
|
||
|
|
|
||
|
|
**Clone When Necessary**:
|
||
|
|
```rust
|
||
|
|
// ✅ Clone for thread safety
|
||
|
|
let user = user.clone();
|
||
|
|
tokio::spawn(async move {
|
||
|
|
process_user(user).await;
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.5 Async/Await
|
||
|
|
|
||
|
|
**Use async/await for IO**:
|
||
|
|
```rust
|
||
|
|
use tokio;
|
||
|
|
|
||
|
|
async fn fetch_user(id: Uuid) -> Result<User> {
|
||
|
|
let user = sqlx::query_as!(
|
||
|
|
User,
|
||
|
|
"SELECT * FROM users WHERE id = $1",
|
||
|
|
id
|
||
|
|
)
|
||
|
|
.fetch_one(&pool)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
Ok(user)
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::main]
|
||
|
|
async fn main() -> Result<()> {
|
||
|
|
let user = fetch_user(uuid).await?;
|
||
|
|
println!("{:?}", user);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Concurrent Operations**:
|
||
|
|
```rust
|
||
|
|
use tokio::try_join;
|
||
|
|
|
||
|
|
async fn fetch_user_data(user_id: Uuid) -> Result<(User, Vec<Track>)> {
|
||
|
|
let user_future = fetch_user(user_id);
|
||
|
|
let tracks_future = fetch_user_tracks(user_id);
|
||
|
|
|
||
|
|
// Run concurrently
|
||
|
|
let (user, tracks) = try_join!(user_future, tracks_future)?;
|
||
|
|
|
||
|
|
Ok((user, tracks))
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.6 Testing
|
||
|
|
|
||
|
|
**Unit Tests**:
|
||
|
|
```rust
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_user_creation() {
|
||
|
|
let user = User::new("test@example.com");
|
||
|
|
assert_eq!(user.email, "test@example.com");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_fetch_user() {
|
||
|
|
let pool = setup_test_db().await;
|
||
|
|
let user = fetch_user(test_uuid, &pool).await.unwrap();
|
||
|
|
assert_eq!(user.email, "test@example.com");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Property-Based Testing** (proptest):
|
||
|
|
```rust
|
||
|
|
use proptest::prelude::*;
|
||
|
|
|
||
|
|
proptest! {
|
||
|
|
#[test]
|
||
|
|
fn test_email_validation(email in "[a-z]+@[a-z]+\\.[a-z]+") {
|
||
|
|
assert!(is_valid_email(&email));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 4. TYPESCRIPT STANDARDS (FRONTEND)
|
||
|
|
|
||
|
|
### 4.1 TypeScript Configuration
|
||
|
|
|
||
|
|
**tsconfig.json** (strict mode):
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"compilerOptions": {
|
||
|
|
"strict": true,
|
||
|
|
"noImplicitAny": true,
|
||
|
|
"strictNullChecks": true,
|
||
|
|
"strictFunctionTypes": true,
|
||
|
|
"noUnusedLocals": true,
|
||
|
|
"noUnusedParameters": true,
|
||
|
|
"noImplicitReturns": true,
|
||
|
|
"noFallthroughCasesInSwitch": true,
|
||
|
|
"esModuleInterop": true,
|
||
|
|
"skipLibCheck": true,
|
||
|
|
"target": "ES2022",
|
||
|
|
"module": "ESNext",
|
||
|
|
"moduleResolution": "bundler",
|
||
|
|
"resolveJsonModule": true,
|
||
|
|
"isolatedModules": true,
|
||
|
|
"jsx": "react-jsx",
|
||
|
|
"baseUrl": ".",
|
||
|
|
"paths": {
|
||
|
|
"@/*": ["./src/*"]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 Type Definitions
|
||
|
|
|
||
|
|
**Prefer Interfaces over Types** (for objects):
|
||
|
|
```typescript
|
||
|
|
// ✅ Good: Interface (can be extended)
|
||
|
|
interface User {
|
||
|
|
id: string;
|
||
|
|
email: string;
|
||
|
|
name: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface AdminUser extends User {
|
||
|
|
permissions: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
// ⚠️ Type (use for unions, intersections)
|
||
|
|
type UserRole = 'user' | 'creator' | 'admin';
|
||
|
|
type Optional<T> = T | null | undefined;
|
||
|
|
```
|
||
|
|
|
||
|
|
**Avoid any**:
|
||
|
|
```typescript
|
||
|
|
// ❌ Bad: any (loses type safety)
|
||
|
|
function processData(data: any) {
|
||
|
|
return data.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Generic
|
||
|
|
function processData<T>(data: T): T {
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: unknown (safe any)
|
||
|
|
function processJSON(json: string): unknown {
|
||
|
|
return JSON.parse(json);
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = processJSON('{"value": 42}');
|
||
|
|
// Must type-check before using
|
||
|
|
if (typeof data === 'object' && data !== null && 'value' in data) {
|
||
|
|
console.log(data.value);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Discriminated Unions**:
|
||
|
|
```typescript
|
||
|
|
type ApiResponse<T> =
|
||
|
|
| { status: 'success'; data: T }
|
||
|
|
| { status: 'error'; error: string };
|
||
|
|
|
||
|
|
function handleResponse<T>(response: ApiResponse<T>) {
|
||
|
|
if (response.status === 'success') {
|
||
|
|
// TypeScript knows response.data exists
|
||
|
|
console.log(response.data);
|
||
|
|
} else {
|
||
|
|
// TypeScript knows response.error exists
|
||
|
|
console.error(response.error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 Function Types
|
||
|
|
|
||
|
|
**Type Function Parameters and Returns**:
|
||
|
|
```typescript
|
||
|
|
// ✅ Good: Typed
|
||
|
|
function calculateTotal(
|
||
|
|
price: number,
|
||
|
|
quantity: number,
|
||
|
|
discount?: number
|
||
|
|
): number {
|
||
|
|
const subtotal = price * quantity;
|
||
|
|
return discount ? subtotal * (1 - discount) : subtotal;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Arrow function
|
||
|
|
const calculateTotal = (
|
||
|
|
price: number,
|
||
|
|
quantity: number,
|
||
|
|
discount = 0
|
||
|
|
): number => {
|
||
|
|
const subtotal = price * quantity;
|
||
|
|
return subtotal * (1 - discount);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**Async Functions**:
|
||
|
|
```typescript
|
||
|
|
async function fetchUser(id: string): Promise<User> {
|
||
|
|
const response = await fetch(`/api/users/${id}`);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch user');
|
||
|
|
}
|
||
|
|
return response.json();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.4 Null Safety
|
||
|
|
|
||
|
|
**Use Optional Chaining**:
|
||
|
|
```typescript
|
||
|
|
// ❌ Bad: Verbose null checks
|
||
|
|
const city = user && user.profile && user.profile.location && user.profile.location.city;
|
||
|
|
|
||
|
|
// ✅ Good: Optional chaining
|
||
|
|
const city = user?.profile?.location?.city;
|
||
|
|
```
|
||
|
|
|
||
|
|
**Use Nullish Coalescing**:
|
||
|
|
```typescript
|
||
|
|
// ❌ Bad: || can give unexpected results (0, '', false treated as falsy)
|
||
|
|
const port = process.env.PORT || 3000; // Problem if PORT=0
|
||
|
|
|
||
|
|
// ✅ Good: ?? only for null/undefined
|
||
|
|
const port = process.env.PORT ?? 3000;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.5 Enums vs Union Types
|
||
|
|
|
||
|
|
**Prefer String Unions** (more type-safe):
|
||
|
|
```typescript
|
||
|
|
// ✅ Good: String union (can't assign invalid values)
|
||
|
|
type UserRole = 'user' | 'creator' | 'premium' | 'moderator' | 'admin';
|
||
|
|
|
||
|
|
const role: UserRole = 'user'; // ✅
|
||
|
|
const invalidRole: UserRole = 'guest'; // ❌ Compile error
|
||
|
|
|
||
|
|
// ⚠️ Enum (runtime overhead, can assign numbers)
|
||
|
|
enum UserRole {
|
||
|
|
User = 'user',
|
||
|
|
Creator = 'creator',
|
||
|
|
Admin = 'admin',
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 5. REACT STANDARDS
|
||
|
|
|
||
|
|
### 5.1 Component Structure
|
||
|
|
|
||
|
|
**Functional Components** (prefer over class):
|
||
|
|
```tsx
|
||
|
|
// ✅ Good: Functional component with TypeScript
|
||
|
|
interface TrackCardProps {
|
||
|
|
track: Track;
|
||
|
|
onPlay: (trackId: string) => void;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const TrackCard: React.FC<TrackCardProps> = ({
|
||
|
|
track,
|
||
|
|
onPlay,
|
||
|
|
className
|
||
|
|
}) => {
|
||
|
|
const [isLiked, setIsLiked] = useState(false);
|
||
|
|
|
||
|
|
const handleLike = useCallback(() => {
|
||
|
|
setIsLiked(!isLiked);
|
||
|
|
// API call...
|
||
|
|
}, [isLiked]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={cn('track-card', className)}>
|
||
|
|
<h3>{track.title}</h3>
|
||
|
|
<button onClick={() => onPlay(track.id)}>Play</button>
|
||
|
|
<button onClick={handleLike}>
|
||
|
|
{isLiked ? '❤️' : '🤍'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.2 Hooks
|
||
|
|
|
||
|
|
**Use Hooks Correctly**:
|
||
|
|
```tsx
|
||
|
|
// ✅ Good: Hooks at top level
|
||
|
|
function UserProfile({ userId }: { userId: string }) {
|
||
|
|
const [user, setUser] = useState<User | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchUser(userId).then(setUser).finally(() => setLoading(false));
|
||
|
|
}, [userId]);
|
||
|
|
|
||
|
|
if (loading) return <Spinner />;
|
||
|
|
if (!user) return <div>User not found</div>;
|
||
|
|
|
||
|
|
return <div>{user.name}</div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ❌ Bad: Conditional hooks
|
||
|
|
function UserProfile({ userId }: { userId: string }) {
|
||
|
|
if (!userId) return null;
|
||
|
|
|
||
|
|
const [user, setUser] = useState<User | null>(null); // ❌ Conditional hook
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Custom Hooks**:
|
||
|
|
```tsx
|
||
|
|
// ✅ Good: Extract reusable logic
|
||
|
|
function useUser(userId: string) {
|
||
|
|
const [user, setUser] = useState<User | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [error, setError] = useState<Error | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setLoading(true);
|
||
|
|
fetchUser(userId)
|
||
|
|
.then(setUser)
|
||
|
|
.catch(setError)
|
||
|
|
.finally(() => setLoading(false));
|
||
|
|
}, [userId]);
|
||
|
|
|
||
|
|
return { user, loading, error };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
function UserProfile({ userId }: { userId: string }) {
|
||
|
|
const { user, loading, error } = useUser(userId);
|
||
|
|
|
||
|
|
if (loading) return <Spinner />;
|
||
|
|
if (error) return <ErrorMessage error={error} />;
|
||
|
|
if (!user) return <div>User not found</div>;
|
||
|
|
|
||
|
|
return <div>{user.name}</div>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**useMemo and useCallback**:
|
||
|
|
```tsx
|
||
|
|
// ✅ Good: Memoize expensive computations
|
||
|
|
function TrackList({ tracks }: { tracks: Track[] }) {
|
||
|
|
const sortedTracks = useMemo(() => {
|
||
|
|
return [...tracks].sort((a, b) => b.playCount - a.playCount);
|
||
|
|
}, [tracks]);
|
||
|
|
|
||
|
|
const handlePlay = useCallback((trackId: string) => {
|
||
|
|
playTrack(trackId);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
{sortedTracks.map(track => (
|
||
|
|
<TrackCard key={track.id} track={track} onPlay={handlePlay} />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.3 State Management
|
||
|
|
|
||
|
|
**Local State** (useState):
|
||
|
|
```tsx
|
||
|
|
function Counter() {
|
||
|
|
const [count, setCount] = useState(0);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<p>Count: {count}</p>
|
||
|
|
<button onClick={() => setCount(count + 1)}>Increment</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Global State** (Zustand):
|
||
|
|
```tsx
|
||
|
|
import { create } from 'zustand';
|
||
|
|
|
||
|
|
interface UserStore {
|
||
|
|
user: User | null;
|
||
|
|
setUser: (user: User) => void;
|
||
|
|
logout: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const useUserStore = create<UserStore>((set) => ({
|
||
|
|
user: null,
|
||
|
|
setUser: (user) => set({ user }),
|
||
|
|
logout: () => set({ user: null }),
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
function Header() {
|
||
|
|
const user = useUserStore((state) => state.user);
|
||
|
|
const logout = useUserStore((state) => state.logout);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<header>
|
||
|
|
{user ? (
|
||
|
|
<>
|
||
|
|
<span>{user.name}</span>
|
||
|
|
<button onClick={logout}>Logout</button>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<a href="/login">Login</a>
|
||
|
|
)}
|
||
|
|
</header>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Server State** (React Query):
|
||
|
|
```tsx
|
||
|
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||
|
|
|
||
|
|
function useTrack(trackId: string) {
|
||
|
|
return useQuery({
|
||
|
|
queryKey: ['track', trackId],
|
||
|
|
queryFn: () => fetchTrack(trackId),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function useLikeTrack() {
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
|
||
|
|
return useMutation({
|
||
|
|
mutationFn: (trackId: string) => likeTrack(trackId),
|
||
|
|
onSuccess: (data, trackId) => {
|
||
|
|
// Invalidate cache
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['track', trackId] });
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
function TrackPage({ trackId }: { trackId: string }) {
|
||
|
|
const { data: track, isLoading, error } = useTrack(trackId);
|
||
|
|
const likeMutation = useLikeTrack();
|
||
|
|
|
||
|
|
if (isLoading) return <Spinner />;
|
||
|
|
if (error) return <ErrorMessage error={error} />;
|
||
|
|
if (!track) return <div>Track not found</div>;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<h1>{track.title}</h1>
|
||
|
|
<button
|
||
|
|
onClick={() => likeMutation.mutate(track.id)}
|
||
|
|
disabled={likeMutation.isPending}
|
||
|
|
>
|
||
|
|
Like
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.4 Component Composition
|
||
|
|
|
||
|
|
**Avoid Prop Drilling**:
|
||
|
|
```tsx
|
||
|
|
// ❌ Bad: Prop drilling
|
||
|
|
function App() {
|
||
|
|
const [user, setUser] = useState<User | null>(null);
|
||
|
|
|
||
|
|
return <Layout user={user} setUser={setUser} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
function Layout({ user, setUser }: { user: User | null; setUser: (user: User) => void }) {
|
||
|
|
return <Header user={user} setUser={setUser} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
function Header({ user, setUser }: { user: User | null; setUser: (user: User) => void }) {
|
||
|
|
return <UserMenu user={user} setUser={setUser} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good: Context or global state
|
||
|
|
function App() {
|
||
|
|
return <Layout />;
|
||
|
|
}
|
||
|
|
|
||
|
|
function Header() {
|
||
|
|
const user = useUserStore((state) => state.user);
|
||
|
|
return <UserMenu />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Compound Components**:
|
||
|
|
```tsx
|
||
|
|
// ✅ Good: Compound pattern
|
||
|
|
interface CardProps {
|
||
|
|
children: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const Card = ({ children }: CardProps) => {
|
||
|
|
return <div className="card">{children}</div>;
|
||
|
|
};
|
||
|
|
|
||
|
|
Card.Header = ({ children }: CardProps) => {
|
||
|
|
return <div className="card-header">{children}</div>;
|
||
|
|
};
|
||
|
|
|
||
|
|
Card.Body = ({ children }: CardProps) => {
|
||
|
|
return <div className="card-body">{children}</div>;
|
||
|
|
};
|
||
|
|
|
||
|
|
Card.Footer = ({ children }: CardProps) => {
|
||
|
|
return <div className="card-footer">{children}</div>;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
<Card>
|
||
|
|
<Card.Header>Title</Card.Header>
|
||
|
|
<Card.Body>Content</Card.Body>
|
||
|
|
<Card.Footer>Actions</Card.Footer>
|
||
|
|
</Card>
|
||
|
|
```
|
||
|
|
|
||
|
|
## 6. CSS/TAILWIND STANDARDS
|
||
|
|
|
||
|
|
### 6.1 Tailwind Utilities
|
||
|
|
|
||
|
|
**Prefer Tailwind Utilities**:
|
||
|
|
```tsx
|
||
|
|
// ✅ Good: Tailwind utilities
|
||
|
|
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow-md">
|
||
|
|
<h2 className="text-xl font-bold text-gray-900">Title</h2>
|
||
|
|
<button className="px-4 py-2 text-white bg-blue-600 rounded hover:bg-blue-700">
|
||
|
|
Action
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
// ❌ Bad: Inline styles
|
||
|
|
<div style={{
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
padding: '16px',
|
||
|
|
backgroundColor: 'white',
|
||
|
|
borderRadius: '8px',
|
||
|
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||
|
|
}}>
|
||
|
|
{/* ... */}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Use cn() Helper** (for conditional classes):
|
||
|
|
```tsx
|
||
|
|
import { cn } from '@/lib/utils';
|
||
|
|
|
||
|
|
interface ButtonProps {
|
||
|
|
variant?: 'primary' | 'secondary';
|
||
|
|
size?: 'sm' | 'md' | 'lg';
|
||
|
|
disabled?: boolean;
|
||
|
|
children: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const Button = ({
|
||
|
|
variant = 'primary',
|
||
|
|
size = 'md',
|
||
|
|
disabled = false,
|
||
|
|
children
|
||
|
|
}: ButtonProps) => {
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
className={cn(
|
||
|
|
'rounded font-semibold transition-colors',
|
||
|
|
{
|
||
|
|
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
|
||
|
|
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
'px-2 py-1 text-sm': size === 'sm',
|
||
|
|
'px-4 py-2 text-base': size === 'md',
|
||
|
|
'px-6 py-3 text-lg': size === 'lg',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
'opacity-50 cursor-not-allowed': disabled,
|
||
|
|
}
|
||
|
|
)}
|
||
|
|
disabled={disabled}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 Custom Components (Extract Reusable)
|
||
|
|
|
||
|
|
**@apply for Component Classes**:
|
||
|
|
```css
|
||
|
|
/* components.css */
|
||
|
|
@layer components {
|
||
|
|
.btn {
|
||
|
|
@apply px-4 py-2 rounded font-semibold transition-colors;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-primary {
|
||
|
|
@apply bg-blue-600 text-white hover:bg-blue-700;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-secondary {
|
||
|
|
@apply bg-gray-200 text-gray-900 hover:bg-gray-300;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card {
|
||
|
|
@apply p-4 bg-white rounded-lg shadow-md;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 7. GIT STANDARDS
|
||
|
|
|
||
|
|
### 7.1 Commit Messages
|
||
|
|
|
||
|
|
**Format**: Conventional Commits
|
||
|
|
```
|
||
|
|
<type>(<scope>): <subject>
|
||
|
|
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<footer>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Types**:
|
||
|
|
```
|
||
|
|
feat: New feature
|
||
|
|
fix: Bug fix
|
||
|
|
docs: Documentation changes
|
||
|
|
style: Code style (formatting, no logic change)
|
||
|
|
refactor: Code refactoring (no feature/bug change)
|
||
|
|
perf: Performance improvements
|
||
|
|
test: Add/update tests
|
||
|
|
chore: Build process, dependencies, tooling
|
||
|
|
```
|
||
|
|
|
||
|
|
**Examples**:
|
||
|
|
```bash
|
||
|
|
# Good commit messages
|
||
|
|
git commit -m "feat(auth): add two-factor authentication"
|
||
|
|
git commit -m "fix(api): resolve rate limiting bug on login endpoint"
|
||
|
|
git commit -m "refactor(user): extract validation logic to separate service"
|
||
|
|
git commit -m "docs(readme): update installation instructions"
|
||
|
|
|
||
|
|
# Bad commit messages
|
||
|
|
git commit -m "fix bug"
|
||
|
|
git commit -m "WIP"
|
||
|
|
git commit -m "update"
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7.2 Branch Naming
|
||
|
|
|
||
|
|
**Format**: `<type>/<ticket>-<description>`
|
||
|
|
```
|
||
|
|
feature/VEZ-123-user-authentication
|
||
|
|
bugfix/VEZ-456-fix-login-error
|
||
|
|
hotfix/VEZ-789-critical-security-patch
|
||
|
|
refactor/VEZ-012-extract-user-service
|
||
|
|
docs/VEZ-345-api-documentation
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7.3 Pull Request Template
|
||
|
|
|
||
|
|
```markdown
|
||
|
|
## Description
|
||
|
|
Brief description of changes
|
||
|
|
|
||
|
|
## Type of Change
|
||
|
|
- [ ] Bug fix
|
||
|
|
- [ ] New feature
|
||
|
|
- [ ] Breaking change
|
||
|
|
- [ ] Documentation update
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
- [ ] Unit tests pass
|
||
|
|
- [ ] Integration tests pass
|
||
|
|
- [ ] Manual testing completed
|
||
|
|
|
||
|
|
## Checklist
|
||
|
|
- [ ] Code follows style guidelines
|
||
|
|
- [ ] Self-review completed
|
||
|
|
- [ ] Documentation updated
|
||
|
|
- [ ] No new warnings
|
||
|
|
```
|
||
|
|
|
||
|
|
## 8. DOCUMENTATION STANDARDS
|
||
|
|
|
||
|
|
### 8.1 Code Comments
|
||
|
|
|
||
|
|
**Go (godoc)**:
|
||
|
|
```go
|
||
|
|
// CreateUser creates a new user in the system.
|
||
|
|
// It validates the request, checks for duplicate emails,
|
||
|
|
// hashes the password, and stores the user in the database.
|
||
|
|
//
|
||
|
|
// Returns the created user and nil error on success.
|
||
|
|
// Returns nil and error if validation fails or database error occurs.
|
||
|
|
func CreateUser(req *CreateUserRequest) (*User, error) {
|
||
|
|
// Validation
|
||
|
|
if err := validateCreateRequest(req); err != nil {
|
||
|
|
return nil, fmt.Errorf("validation failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ... implementation
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Rust (rustdoc)**:
|
||
|
|
```rust
|
||
|
|
/// Creates a new user in the system.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `email` - The user's email address
|
||
|
|
/// * `password` - The user's plain-text password (will be hashed)
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Returns `Ok(User)` on success, or `Err(UserError)` if:
|
||
|
|
/// - Email is invalid
|
||
|
|
/// - Email already exists
|
||
|
|
/// - Database error occurs
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let user = create_user("test@example.com", "SecurePass123").await?;
|
||
|
|
/// assert_eq!(user.email, "test@example.com");
|
||
|
|
/// ```
|
||
|
|
pub async fn create_user(email: &str, password: &str) -> Result<User, UserError> {
|
||
|
|
// Implementation...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**TypeScript (JSDoc)**:
|
||
|
|
```typescript
|
||
|
|
/**
|
||
|
|
* Fetches a user by ID from the API.
|
||
|
|
*
|
||
|
|
* @param userId - The UUID of the user to fetch
|
||
|
|
* @returns A Promise that resolves to the User object
|
||
|
|
* @throws {Error} If the user is not found or API request fails
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* const user = await fetchUser('550e8400-e29b-41d4-a716-446655440000');
|
||
|
|
* console.log(user.name);
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export async function fetchUser(userId: string): Promise<User> {
|
||
|
|
const response = await fetch(`/api/users/${userId}`);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch user');
|
||
|
|
}
|
||
|
|
return response.json();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8.2 README Files
|
||
|
|
|
||
|
|
**Every Module/Package Needs README.md**:
|
||
|
|
```markdown
|
||
|
|
# User Service
|
||
|
|
|
||
|
|
User management service for Veza platform.
|
||
|
|
|
||
|
|
## Features
|
||
|
|
|
||
|
|
- User registration with email verification
|
||
|
|
- Password authentication with bcrypt
|
||
|
|
- JWT token generation
|
||
|
|
- Two-factor authentication (TOTP)
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
```go
|
||
|
|
import "veza/internal/services"
|
||
|
|
|
||
|
|
userService := services.NewUserService(db, redis)
|
||
|
|
user, err := userService.CreateUser(&CreateUserRequest{
|
||
|
|
Email: "test@example.com",
|
||
|
|
Password: "SecurePass123",
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
```bash
|
||
|
|
go test ./... -v
|
||
|
|
```
|
||
|
|
|
||
|
|
## Dependencies
|
||
|
|
|
||
|
|
- GORM (database ORM)
|
||
|
|
- bcrypt (password hashing)
|
||
|
|
- jwt-go (JWT tokens)
|
||
|
|
```
|
||
|
|
|
||
|
|
## 9. CODE REVIEW PROCESS
|
||
|
|
|
||
|
|
### 9.1 Review Checklist
|
||
|
|
|
||
|
|
**Reviewer Must Check**:
|
||
|
|
- [ ] Code follows style guidelines (linters pass)
|
||
|
|
- [ ] Tests are included and pass
|
||
|
|
- [ ] Documentation is updated
|
||
|
|
- [ ] No security vulnerabilities (SQL injection, XSS, etc.)
|
||
|
|
- [ ] Error handling is complete
|
||
|
|
- [ ] Performance is acceptable (no N+1 queries, etc.)
|
||
|
|
- [ ] Breaking changes are documented
|
||
|
|
|
||
|
|
### 9.2 Review Comments
|
||
|
|
|
||
|
|
**Constructive Feedback**:
|
||
|
|
```
|
||
|
|
✅ Good:
|
||
|
|
"Consider extracting this logic into a separate function for better testability:
|
||
|
|
func validateEmail(email string) error { ... }"
|
||
|
|
|
||
|
|
❌ Bad:
|
||
|
|
"This code is terrible."
|
||
|
|
```
|
||
|
|
|
||
|
|
**Approval Process**:
|
||
|
|
- 2 approvals required (1 from senior engineer)
|
||
|
|
- All comments resolved
|
||
|
|
- CI/CD passes
|
||
|
|
|
||
|
|
## 10. REFACTORING GUIDELINES
|
||
|
|
|
||
|
|
### 10.1 When to Refactor
|
||
|
|
|
||
|
|
**Triggers**:
|
||
|
|
- Duplicated code (DRY violation)
|
||
|
|
- Functions > 50 lines
|
||
|
|
- Cyclomatic complexity > 10
|
||
|
|
- Test coverage < 80%
|
||
|
|
- Code smells (magic numbers, deep nesting)
|
||
|
|
|
||
|
|
**Boy Scout Rule**: Leave code cleaner than you found it
|
||
|
|
|
||
|
|
### 10.2 Refactoring Techniques
|
||
|
|
|
||
|
|
**Extract Method**:
|
||
|
|
```go
|
||
|
|
// Before
|
||
|
|
func ProcessOrder(order *Order) error {
|
||
|
|
// Validate order (20 lines)
|
||
|
|
// ...
|
||
|
|
|
||
|
|
// Calculate total (15 lines)
|
||
|
|
// ...
|
||
|
|
|
||
|
|
// Process payment (25 lines)
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
|
||
|
|
// After
|
||
|
|
func ProcessOrder(order *Order) error {
|
||
|
|
if err := validateOrder(order); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
total := calculateTotal(order)
|
||
|
|
|
||
|
|
if err := processPayment(order, total); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 11. ANTI-PATTERNS LIBRARY
|
||
|
|
|
||
|
|
### 11.1 God Object
|
||
|
|
|
||
|
|
**Problem**: One class/module does everything
|
||
|
|
|
||
|
|
**Solution**: Split into smaller, focused modules (SRP)
|
||
|
|
|
||
|
|
### 11.2 Spaghetti Code
|
||
|
|
|
||
|
|
**Problem**: Tangled, unstructured code
|
||
|
|
|
||
|
|
**Solution**: Use clear architecture (Clean Architecture, layered)
|
||
|
|
|
||
|
|
### 11.3 Magic Numbers
|
||
|
|
|
||
|
|
**Problem**: Unexplained literal values
|
||
|
|
```go
|
||
|
|
// ❌ Bad
|
||
|
|
if user.Age > 18 && user.Age < 65 {
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Good
|
||
|
|
const (
|
||
|
|
MinimumAdultAge = 18
|
||
|
|
RetirementAge = 65
|
||
|
|
)
|
||
|
|
|
||
|
|
if user.Age >= MinimumAdultAge && user.Age < RetirementAge {
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 11.4 Premature Optimization
|
||
|
|
|
||
|
|
**Problem**: Optimizing before identifying bottlenecks
|
||
|
|
|
||
|
|
**Solution**: Profile first, then optimize
|
||
|
|
|
||
|
|
## 12. ERROR PREVENTION
|
||
|
|
|
||
|
|
### 12.1 Pre-Flight Checks (OBLIGATOIRE)
|
||
|
|
|
||
|
|
**Avant de commencer TOUTE nouvelle tâche** :
|
||
|
|
|
||
|
|
- [ ] Exécuter `./scripts/pre-flight-check.sh`
|
||
|
|
- [ ] Vérifier qu'aucune erreur P0/P1 existe
|
||
|
|
- [ ] Tests existants passent
|
||
|
|
- [ ] Linter clean
|
||
|
|
- [ ] Code à jour avec `main`
|
||
|
|
|
||
|
|
**Référence complète** : `docs/ORIGIN/ORIGIN_ERROR_PREVENTION_GUIDE.md`
|
||
|
|
|
||
|
|
### 12.2 Templates de Code (OBLIGATOIRE)
|
||
|
|
|
||
|
|
**Utiliser les templates validés** pour créer de nouveaux fichiers :
|
||
|
|
|
||
|
|
- Backend Go : `dev-environment/templates/backend-*.template.go`
|
||
|
|
- Frontend React : `dev-environment/templates/frontend-*.template.tsx`
|
||
|
|
- Rust : `dev-environment/templates/rust-*.template.rs`
|
||
|
|
|
||
|
|
**NE JAMAIS créer un fichier sans utiliser un template** (sauf exception approuvée).
|
||
|
|
|
||
|
|
### 12.3 Patterns Sûrs
|
||
|
|
|
||
|
|
**Backend Go** :
|
||
|
|
- ✅ Interfaces dans `internal/types/` ou `internal/interfaces/`
|
||
|
|
- ✅ Services dépendent uniquement d'interfaces
|
||
|
|
- ✅ Handlers dépendent uniquement d'interfaces
|
||
|
|
- ✅ Types cohérents (toujours `string` OU toujours `*string`)
|
||
|
|
|
||
|
|
**Frontend TypeScript/React** :
|
||
|
|
- ✅ Types explicites pour toutes les fonctions
|
||
|
|
- ✅ Self-closing tags JSX (`<Component />`)
|
||
|
|
- ✅ Mocks configurés pour tous les tests
|
||
|
|
- ✅ Logger au lieu de `console.log`
|
||
|
|
|
||
|
|
**Référence** : `docs/ORIGIN/ORIGIN_ERROR_PATTERNS.md` pour tous les patterns.
|
||
|
|
|
||
|
|
### 12.4 Quality Gates
|
||
|
|
|
||
|
|
**Pre-Commit** (Husky) :
|
||
|
|
- Formatage automatique
|
||
|
|
- Linter (zero errors)
|
||
|
|
- Tests unitaires rapides
|
||
|
|
- Type checking
|
||
|
|
|
||
|
|
**Pre-Merge** (GitHub Actions) :
|
||
|
|
- Architecture validation (import cycles)
|
||
|
|
- Type safety
|
||
|
|
- Test coverage ≥ 80%
|
||
|
|
- Linter validation
|
||
|
|
- Build validation
|
||
|
|
|
||
|
|
**Si un gate échoue** : Corriger l'erreur, NE PAS contourner.
|
||
|
|
|
||
|
|
### 12.5 Documentation des Erreurs
|
||
|
|
|
||
|
|
**Si une nouvelle erreur est découverte** :
|
||
|
|
|
||
|
|
1. Documenter dans `docs/ORIGIN/ORIGIN_ERROR_PATTERNS.md`
|
||
|
|
2. Ajouter la solution standard
|
||
|
|
3. Mettre à jour les checklists de prévention
|
||
|
|
4. Communiquer à l'équipe
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ✅ CHECKLIST DE VALIDATION
|
||
|
|
|
||
|
|
### Code Quality
|
||
|
|
- [ ] Formatters run (gofmt, rustfmt, Prettier)
|
||
|
|
- [ ] Linters pass (golangci-lint, clippy, ESLint)
|
||
|
|
- [ ] Tests written (coverage ≥ 80%)
|
||
|
|
- [ ] Documentation updated (README, comments)
|
||
|
|
|
||
|
|
### Error Prevention
|
||
|
|
- [ ] Pre-flight check exécuté et passé
|
||
|
|
- [ ] Template utilisé pour nouveau fichier
|
||
|
|
- [ ] Patterns sûrs suivis
|
||
|
|
- [ ] Aucun anti-pattern introduit
|
||
|
|
|
||
|
|
### Architecture
|
||
|
|
- [ ] Clean Architecture followed
|
||
|
|
- [ ] SOLID principles respected
|
||
|
|
- [ ] Dependencies injected
|
||
|
|
- [ ] Separation of concerns
|
||
|
|
- [ ] No import cycles
|
||
|
|
|
||
|
|
### Performance
|
||
|
|
- [ ] No N+1 queries
|
||
|
|
- [ ] Expensive operations optimized
|
||
|
|
- [ ] Caching considered
|
||
|
|
- [ ] Bundle size acceptable (frontend)
|
||
|
|
|
||
|
|
### Security
|
||
|
|
- [ ] Input validated
|
||
|
|
- [ ] Errors handled
|
||
|
|
- [ ] Secrets not committed
|
||
|
|
- [ ] SQL injection prevented
|
||
|
|
|
||
|
|
## 📊 MÉTRIQUES DE SUCCÈS
|
||
|
|
|
||
|
|
### Code Quality Metrics
|
||
|
|
- **Cyclomatic Complexity**: < 10 per function
|
||
|
|
- **Function Length**: < 50 lines
|
||
|
|
- **Test Coverage**: ≥ 80%
|
||
|
|
- **Code Duplication**: < 3%
|
||
|
|
- **Linter Warnings**: 0
|
||
|
|
|
||
|
|
## 🔄 HISTORIQUE DES VERSIONS
|
||
|
|
|
||
|
|
| Version | Date | Changements |
|
||
|
|
|---------|------|-------------|
|
||
|
|
| 1.0.0 | 2025-11-02 | Version initiale - Standards complets |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ AVERTISSEMENT
|
||
|
|
|
||
|
|
**CES STANDARDS SONT IMMUABLES**
|
||
|
|
|
||
|
|
Les standards de code définis ici sont **OBLIGATOIRES**. Toute dérogation nécessite:
|
||
|
|
|
||
|
|
1. **RFC Code Standards Exception** avec justification
|
||
|
|
2. **Approbation Lead Engineer**
|
||
|
|
3. **Documentation de l'exception**
|
||
|
|
|
||
|
|
**Le non-respect des standards bloque la merge de la PR.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Document créé par**: Engineering Team
|
||
|
|
**Date de création**: 2025-11-02
|
||
|
|
**Prochaine révision**: Annuelle
|
||
|
|
**Propriétaire**: Lead Engineers (Go, Rust, Frontend)
|
||
|
|
|
||
|
|
**Statut**: ✅ **APPROUVÉ ET VERROUILLÉ**
|