docs: Add comprehensive system architecture diagrams
Created detailed ASCII diagrams documenting the entire system architecture: 1. System Architecture (_go-learning/diagrams/01-system-architecture.md) - Overall system architecture with client/server/storage layers - Layered architecture (Presentation → Application → Business → Data) - Component interaction and HTTP request flow - Data flow from app start through per-request lifecycle - Package dependencies and file organization
This commit is contained in:
@@ -0,0 +1,272 @@
|
|||||||
|
# System Architecture Diagram
|
||||||
|
|
||||||
|
## Overall System Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CV Website System │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Client │────────▶│ Server │───────▶│ Storage │ │
|
||||||
|
│ │ Browser │◀────────│ (Bun/Go) │◀───────│ (Static) │ │
|
||||||
|
│ └────────────┘ └────────────┘ └──────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ HTMX │ Templates │ JSON │
|
||||||
|
│ │ HTTP │ Rendering │ Files │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ UI/UX │ │ Handlers │ │ Data Models │ │
|
||||||
|
│ │ Components │ │ Middleware │ │ CV/UI │ │
|
||||||
|
│ └────────────┘ └────────────┘ └──────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layered Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Presentation Layer │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ HTML Templates + HTMX + Hyperscript + CSS │ │
|
||||||
|
│ │ - Server-side rendering │ │
|
||||||
|
│ │ - Hypermedia-driven architecture │ │
|
||||||
|
│ │ - Progressive enhancement │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Application Layer │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ HTTP Handlers (internal/handlers/) │ │
|
||||||
|
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
|
||||||
|
│ │ │ cv_pages.go │ cv_htmx.go │ cv_pdf.go │ │ │
|
||||||
|
│ │ │ Page render │ HTMX toggles │ PDF export │ │ │
|
||||||
|
│ │ └──────────────┴──────────────┴──────────────┘ │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Middleware Chain (internal/middleware/) │ │
|
||||||
|
│ │ Recovery → Logger → SecurityHeaders → Preferences │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Business Layer │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Data Models (internal/models/) │ │
|
||||||
|
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
|
||||||
|
│ │ │ cv/ │ ui/ │ Validation │ │ │
|
||||||
|
│ │ │ CV data │ UI strings │ Rules │ │ │
|
||||||
|
│ │ └──────────────┴──────────────┴──────────────┘ │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Services (internal/pdf/, internal/lang/) │ │
|
||||||
|
│ │ - PDF generation (chromedp) │ │
|
||||||
|
│ │ - Language handling │ │
|
||||||
|
│ │ - Template management │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Layer │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Static Files │ │
|
||||||
|
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
|
||||||
|
│ │ │ data/ │ templates/ │ static/ │ │ │
|
||||||
|
│ │ │ cv-*.json │ *.html │ css/js/ │ │ │
|
||||||
|
│ │ │ ui-*.json │ partials/ │ images/ │ │ │
|
||||||
|
│ │ └──────────────┴──────────────┴──────────────┘ │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Interaction
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ HTTP Request Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Client Request
|
||||||
|
│
|
||||||
|
├─→ Browser sends HTTP/HTMX request
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ Router │ Match URL pattern
|
||||||
|
│ (ServeMux) │ ├─ / → Home
|
||||||
|
└─────────────┘ ├─ /cv → CVContent
|
||||||
|
│ ├─ /toggle/* → HTMX handlers
|
||||||
|
▼ └─ /export/pdf → ExportPDF
|
||||||
|
┌─────────────┐
|
||||||
|
│ Middleware │ Execute middleware chain
|
||||||
|
│ Chain │ ├─ Recovery (panic handler)
|
||||||
|
└─────────────┘ ├─ Logger (request logging)
|
||||||
|
│ ├─ SecurityHeaders (CSP, HSTS)
|
||||||
|
▼ └─ PreferencesMiddleware (cookies → context)
|
||||||
|
┌─────────────┐
|
||||||
|
│ Handler │ Process request
|
||||||
|
│ Function │ ├─ Parse request (typed)
|
||||||
|
└─────────────┘ ├─ Load data (models)
|
||||||
|
│ ├─ Prepare template data
|
||||||
|
▼ └─ Render response
|
||||||
|
┌─────────────┐
|
||||||
|
│ Template │ Server-side rendering
|
||||||
|
│ Rendering │ ├─ Load template
|
||||||
|
└─────────────┘ ├─ Execute with data
|
||||||
|
│ └─ Generate HTML
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ Response │ Send to client
|
||||||
|
│ (HTML/PDF) │ └─ HTTP 200 OK
|
||||||
|
└─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Client receives response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Flow Diagram │
|
||||||
|
└────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Application Start
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Load Configuration (config.Load()) │
|
||||||
|
│ ├─ Server settings (port, timeouts) │
|
||||||
|
│ └─ Template settings (dir, hot reload) │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Initialize Template Manager │
|
||||||
|
│ ├─ Scan template directory │
|
||||||
|
│ ├─ Parse all templates │
|
||||||
|
│ └─ Cache compiled templates │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Initialize Handlers │
|
||||||
|
│ └─ CVHandler with template manager │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Setup Routes + Middleware │
|
||||||
|
│ └─ routes.Setup(cvHandler, ...) │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Start HTTP Server │
|
||||||
|
│ └─ Listen on :8080 │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Ready for Requests
|
||||||
|
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Per-Request Flow
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Request arrives │
|
||||||
|
│ └─ GET /?lang=es │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ PreferencesMiddleware reads cookies │
|
||||||
|
│ ├─ cv-length = "long" │
|
||||||
|
│ ├─ cv-icons = "show" │
|
||||||
|
│ ├─ cv-language = "es" │
|
||||||
|
│ └─ Store in request context │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Handler.Home() called │
|
||||||
|
│ ├─ Get preferences from context │
|
||||||
|
│ ├─ Validate language │
|
||||||
|
│ └─ Call prepareTemplateData("es") │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Load Data │
|
||||||
|
│ ├─ cvmodel.LoadCV("es") │
|
||||||
|
│ │ └─ Read data/cv-es.json │
|
||||||
|
│ └─ uimodel.LoadUI("es") │
|
||||||
|
│ └─ Read data/ui-es.json │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Process Data │
|
||||||
|
│ ├─ Calculate durations │
|
||||||
|
│ ├─ Split skills into columns │
|
||||||
|
│ └─ Add SEO metadata │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Render Template │
|
||||||
|
│ ├─ Get cached template │
|
||||||
|
│ ├─ Execute with data map │
|
||||||
|
│ └─ Generate HTML │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Send Response │
|
||||||
|
│ └─ HTTP 200 + HTML body │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
main.go
|
||||||
|
├─→ internal/config
|
||||||
|
├─→ internal/templates
|
||||||
|
├─→ internal/handlers
|
||||||
|
│ ├─→ internal/middleware
|
||||||
|
│ ├─→ internal/models/cv
|
||||||
|
│ ├─→ internal/models/ui
|
||||||
|
│ ├─→ internal/pdf
|
||||||
|
│ └─→ internal/templates
|
||||||
|
├─→ internal/routes
|
||||||
|
│ ├─→ internal/handlers
|
||||||
|
│ └─→ internal/middleware
|
||||||
|
└─→ internal/middleware
|
||||||
|
|
||||||
|
internal/handlers/
|
||||||
|
├─ cv.go (constructor)
|
||||||
|
├─ cv_pages.go (renders)
|
||||||
|
├─ cv_htmx.go (toggles)
|
||||||
|
├─ cv_pdf.go (PDF export)
|
||||||
|
├─ cv_helpers.go (utilities)
|
||||||
|
├─ types.go (request/response)
|
||||||
|
└─ errors.go (error handling)
|
||||||
|
|
||||||
|
internal/middleware/
|
||||||
|
└─ preferences.go (cookie → context)
|
||||||
|
|
||||||
|
internal/models/
|
||||||
|
├─ cv/ (CV data structures)
|
||||||
|
└─ ui/ (UI text structures)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [Request Flow](./02-request-flow.md) - Detailed HTTP request lifecycle
|
||||||
|
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution order
|
||||||
|
- [Handler Organization](./04-handler-organization.md) - Handler file structure
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
# Request Flow Diagram
|
||||||
|
|
||||||
|
## Complete HTTP Request Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Full Request Lifecycle │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Client Browser
|
||||||
|
│
|
||||||
|
├─→ User visits /?lang=es&cv-length=long
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HTTP Request │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ GET /?lang=es&cv-length=long HTTP/1.1 │ │
|
||||||
|
│ │ Host: localhost:8080 │ │
|
||||||
|
│ │ Cookie: cv-length=short; cv-icons=show │ │
|
||||||
|
│ │ Accept: text/html │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Go HTTP Server (net/http) │
|
||||||
|
│ ├─ Port :8080 │
|
||||||
|
│ ├─ ServeMux Router │
|
||||||
|
│ └─ Match route pattern │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ MIDDLEWARE CHAIN (4 layers) │
|
||||||
|
│ │
|
||||||
|
│ 1. Recovery Middleware │
|
||||||
|
│ └─→ Wraps entire request in defer/recover │
|
||||||
|
│ │
|
||||||
|
│ 2. Logger Middleware │
|
||||||
|
│ └─→ Logs: [GET] / 127.0.0.1 │
|
||||||
|
│ │
|
||||||
|
│ 3. SecurityHeaders Middleware │
|
||||||
|
│ └─→ Sets: CSP, X-Frame-Options, etc. │
|
||||||
|
│ │
|
||||||
|
│ 4. PreferencesMiddleware │
|
||||||
|
│ ├─→ Reads cookies │
|
||||||
|
│ ├─→ Migrates old values │
|
||||||
|
│ └─→ Stores in request context │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ROUTER (ServeMux) │
|
||||||
|
│ ├─ Pattern: / │
|
||||||
|
│ ├─ Match: Home handler │
|
||||||
|
│ └─ Call: handler.Home(w, r) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HANDLER: CVHandler.Home() │
|
||||||
|
│ (internal/handlers/cv_pages.go) │
|
||||||
|
│ │
|
||||||
|
│ Step 1: Get preferences from context │
|
||||||
|
│ ├─→ prefs := middleware.GetPreferences(r) │
|
||||||
|
│ └─→ Result: CVLength="long", CVLanguage="es" │
|
||||||
|
│ │
|
||||||
|
│ Step 2: Validate language from query params │
|
||||||
|
│ ├─→ lang := r.URL.Query().Get("lang") │
|
||||||
|
│ ├─→ Fallback to: prefs.CVLanguage if empty │
|
||||||
|
│ └─→ Validate: must be "en" or "es" │
|
||||||
|
│ │
|
||||||
|
│ Step 3: Prepare template data │
|
||||||
|
│ ├─→ Call: h.prepareTemplateData(lang) │
|
||||||
|
│ └─→ Returns: map with CV, UI, preferences │
|
||||||
|
│ │
|
||||||
|
│ Step 4: Render template │
|
||||||
|
│ ├─→ Call: h.tmpl.Render(w, "index.html", data) │
|
||||||
|
│ └─→ Returns: HTML response │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ TEMPLATE PREPARATION │
|
||||||
|
│ (prepareTemplateData helper) │
|
||||||
|
│ │
|
||||||
|
│ 1. Load CV data │
|
||||||
|
│ ├─→ cv, err := cvmodel.LoadCV(lang) │
|
||||||
|
│ └─→ Read: data/cv-es.json │
|
||||||
|
│ │
|
||||||
|
│ 2. Load UI strings │
|
||||||
|
│ ├─→ ui, err := uimodel.LoadUI(lang) │
|
||||||
|
│ └─→ Read: data/ui-es.json │
|
||||||
|
│ │
|
||||||
|
│ 3. Calculate experience durations │
|
||||||
|
│ └─→ For each experience: years/months │
|
||||||
|
│ │
|
||||||
|
│ 4. Split skills into columns │
|
||||||
|
│ └─→ Distribute skills evenly across columns │
|
||||||
|
│ │
|
||||||
|
│ 5. Build data map │
|
||||||
|
│ └─→ Return: CV, UI, preferences, SEO metadata │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ TEMPLATE RENDERING │
|
||||||
|
│ (internal/templates/manager.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Get cached template │
|
||||||
|
│ ├─→ tmpl := m.templates["index.html"] │
|
||||||
|
│ └─→ (or reload if hot reload enabled) │
|
||||||
|
│ │
|
||||||
|
│ 2. Execute template │
|
||||||
|
│ ├─→ tmpl.Execute(w, data) │
|
||||||
|
│ ├─→ Process: {{.CV.Name}}, {{range .CV.Experience}} │
|
||||||
|
│ └─→ Include partials: header, footer, sections │
|
||||||
|
│ │
|
||||||
|
│ 3. Generate HTML │
|
||||||
|
│ └─→ Full HTML page with data injected │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ RESPONSE GENERATION │
|
||||||
|
│ │
|
||||||
|
│ Headers: │
|
||||||
|
│ ├─ Content-Type: text/html; charset=utf-8 │
|
||||||
|
│ ├─ Content-Security-Policy: [CSP rules] │
|
||||||
|
│ ├─ X-Frame-Options: DENY │
|
||||||
|
│ └─ Set-Cookie: cv-language=es; Path=/; Max-Age=... │
|
||||||
|
│ │
|
||||||
|
│ Body: │
|
||||||
|
│ └─ <!DOCTYPE html> │
|
||||||
|
│ <html lang="es"> │
|
||||||
|
│ <head>...</head> │
|
||||||
|
│ <body> │
|
||||||
|
│ <!-- Full CV content --> │
|
||||||
|
│ </body> │
|
||||||
|
│ </html> │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ LOGGER MIDDLEWARE (completion) │
|
||||||
|
│ └─→ Log: Completed in 45ms (status: 200) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Client Browser receives HTML
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTMX Toggle Request Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HTMX Toggle Request (Partial Update) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
User clicks toggle button
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HTMX Request │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ GET /toggle/length?current=short HTTP/1.1 │ │
|
||||||
|
│ │ HX-Request: true │ │
|
||||||
|
│ │ HX-Trigger: toggle-length-btn │ │
|
||||||
|
│ │ HX-Target: #main-content │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Middleware Chain (same as above)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ROUTER │
|
||||||
|
│ ├─ Pattern: /toggle/length │
|
||||||
|
│ └─ Handler: CVHandler.ToggleCVLength(w, r) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HANDLER: CVHandler.ToggleCVLength() │
|
||||||
|
│ (internal/handlers/cv_htmx.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Get current preferences │
|
||||||
|
│ └─→ prefs := middleware.GetPreferences(r) │
|
||||||
|
│ │
|
||||||
|
│ 2. Toggle state │
|
||||||
|
│ ├─→ currentLength := prefs.CVLength │
|
||||||
|
│ └─→ newLength := "long" if current == "short" │
|
||||||
|
│ │
|
||||||
|
│ 3. Save new preference │
|
||||||
|
│ └─→ middleware.SetPreferenceCookie(w, "cv-length", newLength) │
|
||||||
|
│ │
|
||||||
|
│ 4. Get language and prepare data │
|
||||||
|
│ └─→ data := h.prepareTemplateData(lang) │
|
||||||
|
│ │
|
||||||
|
│ 5. Render partial template │
|
||||||
|
│ └─→ h.tmpl.Render(w, "partials/cv_content.html", data) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ PARTIAL TEMPLATE RENDERING │
|
||||||
|
│ └─ Only renders: partials/cv_content.html │
|
||||||
|
│ (Not full page, just the content section) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HTMX Response │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ HTTP/1.1 200 OK │ │
|
||||||
|
│ │ Content-Type: text/html │ │
|
||||||
|
│ │ Set-Cookie: cv-length=long; Path=/; Max-Age=... │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ <div id="main-content"> │ │
|
||||||
|
│ │ <!-- Updated CV content with long format --> │ │
|
||||||
|
│ │ </div> │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
HTMX swaps content in #main-content
|
||||||
|
(No page reload, instant update)
|
||||||
|
```
|
||||||
|
|
||||||
|
## PDF Export Request Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Export Request Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
User clicks "Export PDF"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HTTP POST Request │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ POST /export/pdf HTTP/1.1 │ │
|
||||||
|
│ │ Content-Type: application/json │ │
|
||||||
|
│ │ Origin: http://localhost:8080 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ { │ │
|
||||||
|
│ │ "lang": "es", │ │
|
||||||
|
│ │ "length": "long", │ │
|
||||||
|
│ │ "icons": "show", │ │
|
||||||
|
│ │ "version": "with_skills" │ │
|
||||||
|
│ │ } │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Global Middleware Chain
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ROUTE-SPECIFIC MIDDLEWARE │
|
||||||
|
│ │
|
||||||
|
│ 1. OriginChecker │
|
||||||
|
│ └─→ Verify same-origin request │
|
||||||
|
│ │
|
||||||
|
│ 2. RateLimiter │
|
||||||
|
│ └─→ Check: 3 requests/min per IP │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HANDLER: CVHandler.ExportPDF() │
|
||||||
|
│ (internal/handlers/cv_pdf.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Parse and validate request │
|
||||||
|
│ ├─→ var req PDFExportRequest │
|
||||||
|
│ ├─→ json.NewDecoder(r.Body).Decode(&req) │
|
||||||
|
│ └─→ Validate: lang, length, icons, version │
|
||||||
|
│ │
|
||||||
|
│ 2. Render HTML for PDF │
|
||||||
|
│ ├─→ Build data map with preferences │
|
||||||
|
│ ├─→ Render to buffer: index.html template │
|
||||||
|
│ └─→ Result: Full HTML page in memory │
|
||||||
|
│ │
|
||||||
|
│ 3. Generate PDF │
|
||||||
|
│ ├─→ Call: pdf.GeneratePDF(htmlContent, pdfOptions) │
|
||||||
|
│ └─→ Uses: chromedp to render HTML → PDF │
|
||||||
|
│ │
|
||||||
|
│ 4. Send PDF response │
|
||||||
|
│ ├─→ Set headers: application/pdf │
|
||||||
|
│ ├─→ Set filename: CV-[Name]-[lang].pdf │
|
||||||
|
│ └─→ Write: PDF bytes to response │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF GENERATION (chromedp) │
|
||||||
|
│ (internal/pdf/generator.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Launch headless Chrome │
|
||||||
|
│ └─→ chromedp.NewContext() │
|
||||||
|
│ │
|
||||||
|
│ 2. Navigate to data URL │
|
||||||
|
│ └─→ Load HTML content │
|
||||||
|
│ │
|
||||||
|
│ 3. Wait for rendering │
|
||||||
|
│ └─→ Ensure fonts, images loaded │
|
||||||
|
│ │
|
||||||
|
│ 4. Generate PDF │
|
||||||
|
│ ├─→ chromedp.PrintToPDF() with options │
|
||||||
|
│ ├─→ A4 size, margins, print background │
|
||||||
|
│ └─→ Return: PDF bytes │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Response │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ HTTP/1.1 200 OK │ │
|
||||||
|
│ │ Content-Type: application/pdf │ │
|
||||||
|
│ │ Content-Disposition: attachment; filename="CV-..." │ │
|
||||||
|
│ │ Content-Length: 245678 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [PDF binary data] │ │
|
||||||
|
│ └───────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Browser triggers download
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Handling Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Request with invalid language
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Handler validation detects error
|
||||||
|
│
|
||||||
|
├─→ Create: InvalidLanguageError("xx")
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ DomainError Created │
|
||||||
|
│ ├─ Code: INVALID_LANGUAGE │
|
||||||
|
│ ├─ Message: "Unsupported language: xx (use 'en' or 'es')" │
|
||||||
|
│ ├─ StatusCode: 400 │
|
||||||
|
│ └─ Field: "lang" │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler.HandleError(w, r, err) │
|
||||||
|
│ (internal/handlers/errors.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Check if DomainError │
|
||||||
|
│ └─→ Extract: code, message, status, field │
|
||||||
|
│ │
|
||||||
|
│ 2. Log error │
|
||||||
|
│ └─→ log.Printf("[ERROR] %s: %s", code, message) │
|
||||||
|
│ │
|
||||||
|
│ 3. Build error response │
|
||||||
|
│ ├─→ Create: ErrorInfo struct │
|
||||||
|
│ └─→ Create: APIResponse wrapper │
|
||||||
|
│ │
|
||||||
|
│ 4. Send error response │
|
||||||
|
│ ├─→ Set status: 400 Bad Request │
|
||||||
|
│ ├─→ Set content-type: application/json │
|
||||||
|
│ └─→ Write: JSON error response │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Response │
|
||||||
|
│ { │
|
||||||
|
│ "success": false, │
|
||||||
|
│ "error": { │
|
||||||
|
│ "code": "INVALID_LANGUAGE", │
|
||||||
|
│ "message": "Unsupported language: xx", │
|
||||||
|
│ "field": "lang" │
|
||||||
|
│ } │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Client receives error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Metrics
|
||||||
|
|
||||||
|
```
|
||||||
|
Typical Request Timings:
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Component Time % │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ Middleware overhead ~350 μs 0.7% │
|
||||||
|
│ ├─ Recovery ~10 ns │
|
||||||
|
│ ├─ Logger ~100 μs │
|
||||||
|
│ ├─ SecurityHeaders ~50 ns │
|
||||||
|
│ └─ Preferences ~200 μs │
|
||||||
|
│ │
|
||||||
|
│ Handler processing ~500 μs 1.0% │
|
||||||
|
│ ├─ Get preferences ~10 μs │
|
||||||
|
│ ├─ Validate input ~50 μs │
|
||||||
|
│ └─ Prepare data ~440 μs │
|
||||||
|
│ │
|
||||||
|
│ Data loading ~2 ms 4.0% │
|
||||||
|
│ ├─ Load CV JSON ~1 ms │
|
||||||
|
│ └─ Load UI JSON ~1 ms │
|
||||||
|
│ │
|
||||||
|
│ Template rendering ~45 ms 90% │
|
||||||
|
│ ├─ Template execution ~40 ms │
|
||||||
|
│ └─ HTML generation ~5 ms │
|
||||||
|
│ │
|
||||||
|
│ Response transmission ~2 ms 4.0% │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ TOTAL REQUEST TIME ~50 ms 100% │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
PDF Export Timings:
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Component Time % │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ Middleware + Handler ~1 ms 0.1% │
|
||||||
|
│ Template rendering ~50 ms 5% │
|
||||||
|
│ Chrome launch/navigation ~200 ms 20% │
|
||||||
|
│ PDF generation ~700 ms 70% │
|
||||||
|
│ Response transmission ~50 ms 5% │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ TOTAL PDF EXPORT TIME ~1 sec 100% │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [System Architecture](./01-system-architecture.md) - Overall system design
|
||||||
|
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution details
|
||||||
|
- [Handler Organization](./04-handler-organization.md) - Handler structure
|
||||||
|
- [Error Handling Flow](./06-error-handling-flow.md) - Error propagation details
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# Middleware Chain Diagram
|
||||||
|
|
||||||
|
## Middleware Execution Order
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP Request
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ MIDDLEWARE CHAIN │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 1. Recovery Middleware │ │
|
||||||
|
│ │ - Catches panics │ │
|
||||||
|
│ │ - Logs stack trace │ │
|
||||||
|
│ │ - Returns 500 error │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 2. Logger Middleware │ │
|
||||||
|
│ │ - Logs request method, path, IP │ │
|
||||||
|
│ │ - Measures request duration │ │
|
||||||
|
│ │ - Logs response status │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 3. SecurityHeaders Middleware │ │
|
||||||
|
│ │ - Sets CSP header │ │
|
||||||
|
│ │ - Sets X-Frame-Options │ │
|
||||||
|
│ │ - Sets X-Content-Type-Options │ │
|
||||||
|
│ │ - Sets Referrer-Policy │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 4. PreferencesMiddleware │ │
|
||||||
|
│ │ - Reads preference cookies │ │
|
||||||
|
│ │ - Migrates old values │ │
|
||||||
|
│ │ - Stores in request context │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
└───────────────────────────┼────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌───────────────┐
|
||||||
|
│ Router │
|
||||||
|
│ (ServeMux) │
|
||||||
|
└───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌───────────────┐
|
||||||
|
│ Handler │
|
||||||
|
└───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detailed Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Request Processing Flow │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Client Request: GET /?lang=es
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ RECOVERY MIDDLEWARE ║
|
||||||
|
╠═══════════════════════════════════════════════════════════╣
|
||||||
|
║ defer func() { ║
|
||||||
|
║ if err := recover(); err != nil { ║
|
||||||
|
║ log error + stack trace ║
|
||||||
|
║ http.Error(w, "Internal Server Error", 500) ║
|
||||||
|
║ } ║
|
||||||
|
║ }() ║
|
||||||
|
║ ║
|
||||||
|
║ next.ServeHTTP(w, r) ────────────────┐ ║
|
||||||
|
╚════════════════════════════════════════│══════════════════╝
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ LOGGER MIDDLEWARE ║
|
||||||
|
╠═══════════════════════════════════════════════════════════╣
|
||||||
|
║ start := time.Now() ║
|
||||||
|
║ log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr) ║
|
||||||
|
║ ║
|
||||||
|
║ wrapped := responseWriter wrapper ║
|
||||||
|
║ next.ServeHTTP(wrapped, r) ──────────┐ ║
|
||||||
|
║ │ ║
|
||||||
|
║ duration := time.Since(start) │ ║
|
||||||
|
║ log.Printf("Completed in %v (status: %d)", duration, status) ║
|
||||||
|
╚═════════════════════════════════════════│════════════════╝
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ SECURITY HEADERS MIDDLEWARE ║
|
||||||
|
╠═══════════════════════════════════════════════════════════╣
|
||||||
|
║ w.Header().Set("Content-Security-Policy", CSP_POLICY) ║
|
||||||
|
║ w.Header().Set("X-Frame-Options", "DENY") ║
|
||||||
|
║ w.Header().Set("X-Content-Type-Options", "nosniff") ║
|
||||||
|
║ w.Header().Set("Referrer-Policy", "strict-origin") ║
|
||||||
|
║ ║
|
||||||
|
║ next.ServeHTTP(w, r) ────────────────┐ ║
|
||||||
|
╚════════════════════════════════════════│══════════════════╝
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ PREFERENCES MIDDLEWARE ║
|
||||||
|
╠═══════════════════════════════════════════════════════════╣
|
||||||
|
║ // Read cookies ║
|
||||||
|
║ prefs := &Preferences{ ║
|
||||||
|
║ CVLength: getCookie(r, "cv-length", "short"), ║
|
||||||
|
║ CVIcons: getCookie(r, "cv-icons", "show"), ║
|
||||||
|
║ CVLanguage: getCookie(r, "cv-language", "en"), ║
|
||||||
|
║ CVTheme: getCookie(r, "cv-theme", "default"), ║
|
||||||
|
║ ColorTheme: getCookie(r, "color-theme", "light"), ║
|
||||||
|
║ } ║
|
||||||
|
║ ║
|
||||||
|
║ // Migrate old values ║
|
||||||
|
║ if prefs.CVLength == "extended" { ║
|
||||||
|
║ prefs.CVLength = "long" ║
|
||||||
|
║ } ║
|
||||||
|
║ ║
|
||||||
|
║ // Store in context ║
|
||||||
|
║ ctx := context.WithValue(r.Context(), PreferencesKey, prefs) ║
|
||||||
|
║ next.ServeHTTP(w, r.WithContext(ctx)) ───┐ ║
|
||||||
|
╚═════════════════════════════════════════════│════════════╝
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ ROUTER HANDLER │
|
||||||
|
│ │
|
||||||
|
│ Matches route │
|
||||||
|
│ Calls handler │
|
||||||
|
└──────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ HANDLER FUNC │
|
||||||
|
│ │
|
||||||
|
│ Processes req │
|
||||||
|
│ Returns resp │
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route-Specific Middleware
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Route-Specific Middleware Example │
|
||||||
|
│ (PDF Export Endpoint) │
|
||||||
|
└────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Global Middleware Chain (all routes)
|
||||||
|
│
|
||||||
|
├─ Recovery
|
||||||
|
├─ Logger
|
||||||
|
├─ SecurityHeaders
|
||||||
|
└─ PreferencesMiddleware
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Router (ServeMux) │
|
||||||
|
│ │
|
||||||
|
│ /export/pdf → pdfHandler (protected) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────────────────┐ │
|
||||||
|
│ │ Route-Specific Chain │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 1. OriginChecker │ │
|
||||||
|
│ │ └─ Verify same origin│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 2. RateLimiter │ │
|
||||||
|
│ │ └─ 3 req/min per IP │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 3. ExportPDF Handler │ │
|
||||||
|
│ │ └─ Generate PDF │ │
|
||||||
|
│ └───────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware Wrapping Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Middleware function signature
|
||||||
|
type Middleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
|
// Wrapping example
|
||||||
|
handler := routes.Setup(cvHandler, healthHandler)
|
||||||
|
// Returns:
|
||||||
|
// Recovery(
|
||||||
|
// Logger(
|
||||||
|
// SecurityHeaders(
|
||||||
|
// PreferencesMiddleware(mux)
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
|
||||||
|
// Execution flow (unwraps from outside to inside):
|
||||||
|
Request
|
||||||
|
↓ enters Recovery
|
||||||
|
↓ enters Logger
|
||||||
|
↓ enters SecurityHeaders
|
||||||
|
↓ enters PreferencesMiddleware
|
||||||
|
↓ enters mux/handler
|
||||||
|
↓ handler processes
|
||||||
|
↑ exits PreferencesMiddleware
|
||||||
|
↑ exits SecurityHeaders
|
||||||
|
↑ exits Logger (logs duration)
|
||||||
|
↑ exits Recovery
|
||||||
|
↑
|
||||||
|
Response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Context Values Through Middleware │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Initial Request Context
|
||||||
|
│
|
||||||
|
├─ Empty context.Background()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
PreferencesMiddleware
|
||||||
|
│
|
||||||
|
├─ Reads cookies
|
||||||
|
├─ Creates Preferences struct
|
||||||
|
└─ Adds to context
|
||||||
|
│
|
||||||
|
└─→ ctx = context.WithValue(r.Context(), PreferencesKey, prefs)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ Modified Request Context │
|
||||||
|
│ │
|
||||||
|
│ PreferencesKey → &Preferences{ │
|
||||||
|
│ CVLength: "long", │
|
||||||
|
│ CVIcons: "show", │
|
||||||
|
│ CVLanguage: "es", │
|
||||||
|
│ CVTheme: "default", │
|
||||||
|
│ ColorTheme: "light", │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Handler receives request with enriched context
|
||||||
|
│
|
||||||
|
├─→ prefs := middleware.GetPreferences(r)
|
||||||
|
│ // Retrieves from context
|
||||||
|
│
|
||||||
|
└─→ lang := middleware.GetLanguage(r)
|
||||||
|
// Helper that calls GetPreferences
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling in Middleware
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Handling Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Recovery Middleware
|
||||||
|
│
|
||||||
|
│ Normal Flow:
|
||||||
|
│ ┌─────────────────────────────────────┐
|
||||||
|
│ │ next.ServeHTTP(w, r) │
|
||||||
|
│ │ ↓ │
|
||||||
|
│ │ Handler processes successfully │
|
||||||
|
│ │ ↓ │
|
||||||
|
│ │ Returns response │
|
||||||
|
│ └─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ Panic Flow:
|
||||||
|
│ ┌─────────────────────────────────────┐
|
||||||
|
│ │ next.ServeHTTP(w, r) │
|
||||||
|
│ │ ↓ │
|
||||||
|
│ │ Handler panics! │
|
||||||
|
│ │ ↓ │
|
||||||
|
│ │ defer recover() catches it │
|
||||||
|
│ │ ↓ │
|
||||||
|
│ │ log.Printf("PANIC: %v\\n%s", │
|
||||||
|
│ │ err, debug.Stack()) │
|
||||||
|
│ │ ↓ │
|
||||||
|
│ │ http.Error(w, "Internal Error", 500)│
|
||||||
|
│ └─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Response to client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
```
|
||||||
|
Middleware Performance Impact (per request):
|
||||||
|
|
||||||
|
Recovery: ~10 ns (defer overhead)
|
||||||
|
Logger: ~100 μs (time measurements, string formatting)
|
||||||
|
SecurityHeaders: ~50 ns (header setting)
|
||||||
|
Preferences: ~200 μs (cookie parsing, context creation)
|
||||||
|
|
||||||
|
Total overhead: ~350 μs per request
|
||||||
|
Handler time: ~1-5 ms (template rendering)
|
||||||
|
|
||||||
|
Total request: ~1.5-5.5 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [System Architecture](./01-system-architecture.md) - Overall system design
|
||||||
|
- [Request Flow](./02-request-flow.md) - Complete HTTP request lifecycle
|
||||||
|
- [Error Handling](./06-error-handling-flow.md) - Error propagation
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
# Handler Organization Diagram
|
||||||
|
|
||||||
|
## Handler File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
internal/handlers/
|
||||||
|
├── cv.go Constructor, shared state
|
||||||
|
├── cv_pages.go Full page renders (Home, CVContent)
|
||||||
|
├── cv_htmx.go HTMX partial updates (4 toggles)
|
||||||
|
├── cv_pdf.go PDF export endpoint
|
||||||
|
├── cv_helpers.go Shared utilities (prepareTemplateData, etc.)
|
||||||
|
├── types.go Request/response types, validation
|
||||||
|
├── errors.go Error handling, domain errors
|
||||||
|
├── cv_pages_test.go Tests for page handlers
|
||||||
|
├── cv_htmx_test.go Tests for HTMX handlers
|
||||||
|
└── benchmarks_test.go Benchmark tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Responsibilities
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ cv.go │
|
||||||
|
│ (Constructor & State) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type CVHandler struct { │
|
||||||
|
│ tmpl *templates.Manager // Template renderer │
|
||||||
|
│ host string // For absolute URLs │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ func NewCVHandler(tmpl, host) *CVHandler │
|
||||||
|
│ └─→ Constructor for handler initialization │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ cv_pages.go │
|
||||||
|
│ (Full Page Renders) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ func (h *CVHandler) Home(w, r) │
|
||||||
|
│ └─→ GET / │
|
||||||
|
│ ├─ Get preferences from context │
|
||||||
|
│ ├─ Validate language parameter │
|
||||||
|
│ ├─ Prepare full template data │
|
||||||
|
│ └─ Render: index.html (full page) │
|
||||||
|
│ │
|
||||||
|
│ func (h *CVHandler) CVContent(w, r) │
|
||||||
|
│ └─→ GET /cv │
|
||||||
|
│ ├─ Get preferences from context │
|
||||||
|
│ ├─ Prepare template data │
|
||||||
|
│ └─ Render: partials/cv_content.html │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ cv_htmx.go │
|
||||||
|
│ (HTMX Partial Updates) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ func (h *CVHandler) ToggleCVLength(w, r) │
|
||||||
|
│ └─→ GET /toggle/length?current=short │
|
||||||
|
│ ├─ Get current preferences │
|
||||||
|
│ ├─ Toggle: short ↔ long │
|
||||||
|
│ ├─ Save cookie: cv-length │
|
||||||
|
│ └─ Render: partials/cv_content.html │
|
||||||
|
│ │
|
||||||
|
│ func (h *CVHandler) ToggleCVIcons(w, r) │
|
||||||
|
│ └─→ GET /toggle/icons?current=show │
|
||||||
|
│ ├─ Toggle: show ↔ hide │
|
||||||
|
│ ├─ Save cookie: cv-icons │
|
||||||
|
│ └─ Render: partials/cv_content.html │
|
||||||
|
│ │
|
||||||
|
│ func (h *CVHandler) ToggleCVTheme(w, r) │
|
||||||
|
│ └─→ GET /toggle/theme?current=default │
|
||||||
|
│ ├─ Toggle: default ↔ minimal │
|
||||||
|
│ ├─ Save cookie: cv-theme │
|
||||||
|
│ └─ Render: partials/cv_content.html │
|
||||||
|
│ │
|
||||||
|
│ func (h *CVHandler) ToggleLanguage(w, r) │
|
||||||
|
│ └─→ GET /toggle/language?current=en │
|
||||||
|
│ ├─ Toggle: en ↔ es │
|
||||||
|
│ ├─ Save cookie: cv-language │
|
||||||
|
│ └─ Render: index.html (full page for i18n) │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ cv_pdf.go │
|
||||||
|
│ (PDF Export) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ func (h *CVHandler) ExportPDF(w, r) │
|
||||||
|
│ └─→ POST /export/pdf │
|
||||||
|
│ ├─ Parse JSON request body │
|
||||||
|
│ ├─ Validate: lang, length, icons, version │
|
||||||
|
│ ├─ Render HTML to buffer │
|
||||||
|
│ ├─ Generate PDF via chromedp │
|
||||||
|
│ └─ Send PDF response with download header │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ cv_helpers.go │
|
||||||
|
│ (Shared Utilities) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ func (h *CVHandler) prepareTemplateData(lang) map │
|
||||||
|
│ └─→ Shared data preparation for all handlers │
|
||||||
|
│ ├─ Load CV data: cvmodel.LoadCV(lang) │
|
||||||
|
│ ├─ Load UI strings: uimodel.LoadUI(lang) │
|
||||||
|
│ ├─ Calculate durations for experiences │
|
||||||
|
│ ├─ Split skills into columns │
|
||||||
|
│ ├─ Add SEO metadata │
|
||||||
|
│ └─ Return: complete data map │
|
||||||
|
│ │
|
||||||
|
│ func (h *CVHandler) getFullURL(path) string │
|
||||||
|
│ └─→ Build absolute URLs for SEO/PDF │
|
||||||
|
│ └─ Return: http://host/path │
|
||||||
|
│ │
|
||||||
|
│ func validateLanguage(lang) error │
|
||||||
|
│ └─→ Validate language parameter │
|
||||||
|
│ └─ Check: lang in ["en", "es"] │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ types.go │
|
||||||
|
│ (Request/Response Types) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ // Request Types │
|
||||||
|
│ type PDFExportRequest struct { │
|
||||||
|
│ Lang string `json:"lang" validate:"required,oneof=en es"` │
|
||||||
|
│ Length string `json:"length" validate:"required,oneof=short long"` │
|
||||||
|
│ Icons string `json:"icons" validate:"required,oneof=show hide"` │
|
||||||
|
│ Version string `json:"version" validate:"required,oneof=with_skills clean"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ // Response Types │
|
||||||
|
│ type APIResponse struct { │
|
||||||
|
│ Success bool `json:"success"` │
|
||||||
|
│ Data interface{} `json:"data,omitempty"` │
|
||||||
|
│ Error *ErrorInfo `json:"error,omitempty"` │
|
||||||
|
│ Meta *MetaInfo `json:"meta,omitempty"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ type ErrorInfo struct { │
|
||||||
|
│ Code string `json:"code"` │
|
||||||
|
│ Message string `json:"message"` │
|
||||||
|
│ Field string `json:"field,omitempty"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ type MetaInfo struct { │
|
||||||
|
│ Timestamp time.Time `json:"timestamp"` │
|
||||||
|
│ RequestID string `json:"request_id,omitempty"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ // Constructor Functions │
|
||||||
|
│ func NewAPIResponse(data interface{}) *APIResponse │
|
||||||
|
│ func NewErrorResponse(code, message string) *APIResponse │
|
||||||
|
│ func NewPDFExportRequest() *PDFExportRequest │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ errors.go │
|
||||||
|
│ (Error Handling) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ // Error Codes │
|
||||||
|
│ type ErrorCode string │
|
||||||
|
│ const ( │
|
||||||
|
│ ErrCodeInvalidLanguage = "INVALID_LANGUAGE" │
|
||||||
|
│ ErrCodeInvalidLength = "INVALID_LENGTH" │
|
||||||
|
│ ErrCodeInvalidIcons = "INVALID_ICONS" │
|
||||||
|
│ ErrCodePDFGeneration = "PDF_GENERATION" │
|
||||||
|
│ ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED" │
|
||||||
|
│ // ... 8 more error codes │
|
||||||
|
│ ) │
|
||||||
|
│ │
|
||||||
|
│ // Domain Error Type │
|
||||||
|
│ type DomainError struct { │
|
||||||
|
│ Code ErrorCode │
|
||||||
|
│ Message string │
|
||||||
|
│ Err error │
|
||||||
|
│ StatusCode int │
|
||||||
|
│ Field string │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ // Error Constructors │
|
||||||
|
│ func InvalidLanguageError(lang) *DomainError │
|
||||||
|
│ func InvalidLengthError(length) *DomainError │
|
||||||
|
│ func PDFGenerationError(err) *DomainError │
|
||||||
|
│ // ... 10 more constructors │
|
||||||
|
│ │
|
||||||
|
│ // Error Handler │
|
||||||
|
│ func (h *CVHandler) HandleError(w, r, err) │
|
||||||
|
│ └─→ Centralized error handling │
|
||||||
|
│ ├─ Log error with code │
|
||||||
|
│ ├─ Build error response │
|
||||||
|
│ └─ Send JSON error │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handler Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler Dependencies │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
CVHandler
|
||||||
|
├─→ internal/templates (template rendering)
|
||||||
|
│ └─→ Manager.Render(w, name, data)
|
||||||
|
│
|
||||||
|
├─→ internal/models/cv (CV data)
|
||||||
|
│ └─→ LoadCV(lang) (*CV, error)
|
||||||
|
│
|
||||||
|
├─→ internal/models/ui (UI strings)
|
||||||
|
│ └─→ LoadUI(lang) (*UI, error)
|
||||||
|
│
|
||||||
|
├─→ internal/middleware (preferences)
|
||||||
|
│ ├─→ GetPreferences(r) *Preferences
|
||||||
|
│ ├─→ GetLanguage(r) string
|
||||||
|
│ ├─→ IsLongCV(r) bool
|
||||||
|
│ └─→ SetPreferenceCookie(w, name, value)
|
||||||
|
│
|
||||||
|
├─→ internal/pdf (PDF generation)
|
||||||
|
│ └─→ GeneratePDF(html, options) ([]byte, error)
|
||||||
|
│
|
||||||
|
└─→ encoding/json (JSON parsing)
|
||||||
|
└─→ json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handler Call Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Typical Handler Call Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Request arrives
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Middleware Chain │
|
||||||
|
│ (preferences set) │
|
||||||
|
└─────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Handler Method │
|
||||||
|
│ (cv_pages.go) │
|
||||||
|
└─────────────────────┘
|
||||||
|
│
|
||||||
|
├─→ middleware.GetPreferences(r)
|
||||||
|
│ └─→ Extract from request context
|
||||||
|
│
|
||||||
|
├─→ validateLanguage(lang)
|
||||||
|
│ └─→ Check valid language
|
||||||
|
│
|
||||||
|
├─→ h.prepareTemplateData(lang)
|
||||||
|
│ │ (cv_helpers.go)
|
||||||
|
│ │
|
||||||
|
│ ├─→ cvmodel.LoadCV(lang)
|
||||||
|
│ │ └─→ Read data/cv-{lang}.json
|
||||||
|
│ │
|
||||||
|
│ ├─→ uimodel.LoadUI(lang)
|
||||||
|
│ │ └─→ Read data/ui-{lang}.json
|
||||||
|
│ │
|
||||||
|
│ ├─→ calculateDurations()
|
||||||
|
│ │ └─→ For each experience
|
||||||
|
│ │
|
||||||
|
│ └─→ splitSkillsIntoColumns()
|
||||||
|
│ └─→ Distribute evenly
|
||||||
|
│
|
||||||
|
└─→ h.tmpl.Render(w, "index.html", data)
|
||||||
|
└─→ Execute template with data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handler Testing Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler Tests │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
cv_pages_test.go
|
||||||
|
├─ TestHome
|
||||||
|
│ ├─ Valid requests (en, es)
|
||||||
|
│ ├─ Invalid language
|
||||||
|
│ ├─ With preferences
|
||||||
|
│ └─ Default fallback
|
||||||
|
│
|
||||||
|
└─ TestCVContent
|
||||||
|
├─ Valid language
|
||||||
|
├─ With preferences
|
||||||
|
└─ Error handling
|
||||||
|
|
||||||
|
cv_htmx_test.go
|
||||||
|
├─ TestToggleCVLength
|
||||||
|
│ ├─ short → long
|
||||||
|
│ ├─ long → short
|
||||||
|
│ └─ Cookie setting
|
||||||
|
│
|
||||||
|
├─ TestToggleCVIcons
|
||||||
|
│ ├─ show → hide
|
||||||
|
│ └─ hide → show
|
||||||
|
│
|
||||||
|
├─ TestToggleCVTheme
|
||||||
|
│ └─ default ↔ minimal
|
||||||
|
│
|
||||||
|
└─ TestToggleLanguage
|
||||||
|
└─ en ↔ es
|
||||||
|
|
||||||
|
benchmarks_test.go
|
||||||
|
├─ BenchmarkHome
|
||||||
|
├─ BenchmarkCVContent
|
||||||
|
├─ BenchmarkToggleCVLength
|
||||||
|
├─ BenchmarkToggleCVIcons
|
||||||
|
├─ BenchmarkToggleCVTheme
|
||||||
|
├─ BenchmarkToggleLanguage
|
||||||
|
├─ BenchmarkExportPDF
|
||||||
|
├─ BenchmarkPrepareTemplateData
|
||||||
|
├─ BenchmarkValidateLanguage
|
||||||
|
├─ BenchmarkErrorResponse
|
||||||
|
└─ BenchmarkNewAPIResponse
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handler Pattern Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler Organization Principles │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
1. SEPARATION BY RESPONSIBILITY
|
||||||
|
├─ Pages: Full page renders
|
||||||
|
├─ HTMX: Partial updates
|
||||||
|
├─ PDF: Export functionality
|
||||||
|
└─ Helpers: Shared utilities
|
||||||
|
|
||||||
|
2. TYPE SAFETY
|
||||||
|
├─ Structured request types
|
||||||
|
├─ Structured response types
|
||||||
|
└─ Validation tags
|
||||||
|
|
||||||
|
3. ERROR HANDLING
|
||||||
|
├─ Domain-specific errors
|
||||||
|
├─ Error codes
|
||||||
|
└─ Centralized error handler
|
||||||
|
|
||||||
|
4. TESTABILITY
|
||||||
|
├─ Unit tests per file
|
||||||
|
├─ Integration tests
|
||||||
|
└─ Benchmark tests
|
||||||
|
|
||||||
|
5. DEPENDENCY INJECTION
|
||||||
|
├─ Template manager injected
|
||||||
|
├─ No global state
|
||||||
|
└─ Easy to mock
|
||||||
|
|
||||||
|
6. MIDDLEWARE INTEGRATION
|
||||||
|
├─ Preferences from context
|
||||||
|
├─ Helper functions
|
||||||
|
└─ Clean separation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Profile
|
||||||
|
|
||||||
|
```
|
||||||
|
Handler Performance Characteristics:
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler Time Allocations │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Home() ~50 ms ~1200 allocs │
|
||||||
|
│ CVContent() ~45 ms ~1100 allocs │
|
||||||
|
│ ToggleCVLength() ~45 ms ~1100 allocs │
|
||||||
|
│ ToggleCVIcons() ~45 ms ~1100 allocs │
|
||||||
|
│ ToggleCVTheme() ~45 ms ~1100 allocs │
|
||||||
|
│ ToggleLanguage() ~50 ms ~1200 allocs │
|
||||||
|
│ ExportPDF() ~1000 ms ~5000 allocs │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ prepareTemplateData() ~2 ms ~50 allocs │
|
||||||
|
│ validateLanguage() ~10 ns 0 allocs │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Memory Profile:
|
||||||
|
- Most allocations in template rendering (~90%)
|
||||||
|
- JSON parsing minimal (<1%)
|
||||||
|
- Helper functions optimized (zero-alloc where possible)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [System Architecture](./01-system-architecture.md) - Overall system design
|
||||||
|
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
|
||||||
|
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution
|
||||||
|
- [Error Handling Flow](./06-error-handling-flow.md) - Error propagation
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
# Data Models Diagram
|
||||||
|
|
||||||
|
## Data Model Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Model Structure │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
internal/models/
|
||||||
|
├── cv/ CV data structures
|
||||||
|
│ ├── cv.go Main CV model
|
||||||
|
│ ├── personal.go Personal information
|
||||||
|
│ ├── experience.go Work experience
|
||||||
|
│ ├── education.go Education history
|
||||||
|
│ ├── skills.go Technical skills
|
||||||
|
│ └── languages.go Language proficiency
|
||||||
|
│
|
||||||
|
└── ui/ UI text structures
|
||||||
|
├── ui.go Main UI model
|
||||||
|
├── sections.go Section titles
|
||||||
|
├── buttons.go Button labels
|
||||||
|
└── messages.go User messages
|
||||||
|
```
|
||||||
|
|
||||||
|
## CV Data Model
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ CV Structure (cv/cv.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type CV struct { │
|
||||||
|
│ Personal Personal `json:"personal"` │
|
||||||
|
│ Summary string `json:"summary"` │
|
||||||
|
│ Experience []Experience `json:"experience"` │
|
||||||
|
│ Education []Education `json:"education"` │
|
||||||
|
│ Skills Skills `json:"skills"` │
|
||||||
|
│ Languages []Language `json:"languages"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ Methods: │
|
||||||
|
│ ├─ LoadCV(lang string) (*CV, error) │
|
||||||
|
│ │ └─→ Read data/cv-{lang}.json │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ Validate() error │
|
||||||
|
│ │ └─→ Ensure all required fields present │
|
||||||
|
│ │ │
|
||||||
|
│ └─ CalculateDurations() │
|
||||||
|
│ └─→ Calculate years/months for experiences │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Personal Information (cv/personal.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Personal struct { │
|
||||||
|
│ Name string `json:"name"` │
|
||||||
|
│ Title string `json:"title"` │
|
||||||
|
│ Email string `json:"email"` │
|
||||||
|
│ Phone string `json:"phone,omitempty"` │
|
||||||
|
│ Location string `json:"location"` │
|
||||||
|
│ Website string `json:"website,omitempty"` │
|
||||||
|
│ LinkedIn string `json:"linkedin,omitempty"` │
|
||||||
|
│ GitHub string `json:"github,omitempty"` │
|
||||||
|
│ Photo string `json:"photo,omitempty"` │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Work Experience (cv/experience.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Experience struct { │
|
||||||
|
│ Company string `json:"company"` │
|
||||||
|
│ Position string `json:"position"` │
|
||||||
|
│ Location string `json:"location"` │
|
||||||
|
│ StartDate string `json:"start_date"` │
|
||||||
|
│ EndDate string `json:"end_date,omitempty"` │
|
||||||
|
│ Current bool `json:"current"` │
|
||||||
|
│ Description string `json:"description"` │
|
||||||
|
│ Highlights []string `json:"highlights"` │
|
||||||
|
│ Duration string `json:"-"` // Calculated │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ Methods: │
|
||||||
|
│ ├─ CalculateDuration() string │
|
||||||
|
│ │ ├─ Parse StartDate and EndDate │
|
||||||
|
│ │ ├─ Calculate difference │
|
||||||
|
│ │ └─ Return: "2 years 3 months" or "Present" │
|
||||||
|
│ │ │
|
||||||
|
│ └─ IsCurrent() bool │
|
||||||
|
│ └─→ Check if EndDate is empty or Current flag │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Education (cv/education.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Education struct { │
|
||||||
|
│ Institution string `json:"institution"` │
|
||||||
|
│ Degree string `json:"degree"` │
|
||||||
|
│ Field string `json:"field"` │
|
||||||
|
│ Location string `json:"location"` │
|
||||||
|
│ StartDate string `json:"start_date"` │
|
||||||
|
│ EndDate string `json:"end_date,omitempty"` │
|
||||||
|
│ GPA string `json:"gpa,omitempty"` │
|
||||||
|
│ Honors []string `json:"honors,omitempty"` │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Skills (cv/skills.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Skills struct { │
|
||||||
|
│ Technical []Skill `json:"technical"` │
|
||||||
|
│ Soft []Skill `json:"soft"` │
|
||||||
|
│ Tools []Skill `json:"tools"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ type Skill struct { │
|
||||||
|
│ Name string `json:"name"` │
|
||||||
|
│ Level string `json:"level,omitempty"` │
|
||||||
|
│ Icon string `json:"icon,omitempty"` │
|
||||||
|
│ Category string `json:"category,omitempty"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ Methods: │
|
||||||
|
│ └─ SplitIntoColumns(numCols int) [][]Skill │
|
||||||
|
│ └─→ Distribute skills evenly across columns │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Languages (cv/languages.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Language struct { │
|
||||||
|
│ Name string `json:"name"` │
|
||||||
|
│ Level string `json:"level"` │
|
||||||
|
│ Proficiency string `json:"proficiency,omitempty"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ Levels: Native, Fluent, Professional, Intermediate, Basic │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI Data Model
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ UI Structure (ui/ui.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type UI struct { │
|
||||||
|
│ Sections Sections `json:"sections"` │
|
||||||
|
│ Buttons Buttons `json:"buttons"` │
|
||||||
|
│ Messages Messages `json:"messages"` │
|
||||||
|
│ Labels Labels `json:"labels"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ Methods: │
|
||||||
|
│ └─ LoadUI(lang string) (*UI, error) │
|
||||||
|
│ └─→ Read data/ui-{lang}.json │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Section Titles (ui/sections.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Sections struct { │
|
||||||
|
│ Summary string `json:"summary"` │
|
||||||
|
│ Experience string `json:"experience"` │
|
||||||
|
│ Education string `json:"education"` │
|
||||||
|
│ Skills string `json:"skills"` │
|
||||||
|
│ Languages string `json:"languages"` │
|
||||||
|
│ Contact string `json:"contact"` │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ Example (English): │
|
||||||
|
│ { │
|
||||||
|
│ "summary": "Professional Summary", │
|
||||||
|
│ "experience": "Work Experience", │
|
||||||
|
│ "education": "Education", │
|
||||||
|
│ "skills": "Technical Skills", │
|
||||||
|
│ "languages": "Languages", │
|
||||||
|
│ "contact": "Contact Information" │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Button Labels (ui/buttons.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Buttons struct { │
|
||||||
|
│ ExportPDF string `json:"export_pdf"` │
|
||||||
|
│ ToggleLength string `json:"toggle_length"` │
|
||||||
|
│ ToggleIcons string `json:"toggle_icons"` │
|
||||||
|
│ ToggleTheme string `json:"toggle_theme"` │
|
||||||
|
│ ToggleLanguage string `json:"toggle_language"` │
|
||||||
|
│ Print string `json:"print"` │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ User Messages (ui/messages.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Messages struct { │
|
||||||
|
│ Loading string `json:"loading"` │
|
||||||
|
│ Error string `json:"error"` │
|
||||||
|
│ Success string `json:"success"` │
|
||||||
|
│ PDFGenerating string `json:"pdf_generating"` │
|
||||||
|
│ PDFReady string `json:"pdf_ready"` │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Labels (ui/labels.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Labels struct { │
|
||||||
|
│ ShortCV string `json:"short_cv"` │
|
||||||
|
│ LongCV string `json:"long_cv"` │
|
||||||
|
│ ShowIcons string `json:"show_icons"` │
|
||||||
|
│ HideIcons string `json:"hide_icons"` │
|
||||||
|
│ Light string `json:"light"` │
|
||||||
|
│ Dark string `json:"dark"` │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
JSON Files (data/)
|
||||||
|
├── cv-en.json English CV data
|
||||||
|
├── cv-es.json Spanish CV data
|
||||||
|
├── ui-en.json English UI strings
|
||||||
|
└── ui-es.json Spanish UI strings
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ LoadCV(lang) │
|
||||||
|
│ LoadUI(lang) │
|
||||||
|
│ (internal/models/) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
├─→ Parse JSON
|
||||||
|
├─→ Validate structure
|
||||||
|
└─→ Return typed structs
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Handler │
|
||||||
|
│ (internal/handlers/) │
|
||||||
|
└─────────────────────────┐
|
||||||
|
│
|
||||||
|
├─→ Calculate durations
|
||||||
|
├─→ Split skills
|
||||||
|
└─→ Build template data map
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Template Rendering │
|
||||||
|
│ (templates/) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
HTML Response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Data Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Sample CV Data (data/cv-en.json) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
{
|
||||||
|
"personal": {
|
||||||
|
"name": "John Doe",
|
||||||
|
"title": "Senior Software Engineer",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"location": "San Francisco, CA",
|
||||||
|
"linkedin": "linkedin.com/in/johndoe",
|
||||||
|
"github": "github.com/johndoe"
|
||||||
|
},
|
||||||
|
"summary": "Experienced software engineer with 8+ years...",
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"company": "Tech Corp",
|
||||||
|
"position": "Senior Software Engineer",
|
||||||
|
"location": "San Francisco, CA",
|
||||||
|
"start_date": "2020-01",
|
||||||
|
"end_date": "",
|
||||||
|
"current": true,
|
||||||
|
"description": "Leading backend development...",
|
||||||
|
"highlights": [
|
||||||
|
"Designed and implemented microservices architecture",
|
||||||
|
"Reduced API response time by 60%",
|
||||||
|
"Mentored 5 junior developers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"education": [
|
||||||
|
{
|
||||||
|
"institution": "University of California",
|
||||||
|
"degree": "Bachelor of Science",
|
||||||
|
"field": "Computer Science",
|
||||||
|
"location": "Berkeley, CA",
|
||||||
|
"start_date": "2012-09",
|
||||||
|
"end_date": "2016-05",
|
||||||
|
"gpa": "3.8/4.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skills": {
|
||||||
|
"technical": [
|
||||||
|
{"name": "Go", "level": "Expert", "icon": "golang"},
|
||||||
|
{"name": "JavaScript", "level": "Advanced", "icon": "js"},
|
||||||
|
{"name": "Python", "level": "Intermediate", "icon": "python"}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
{"name": "Docker", "icon": "docker"},
|
||||||
|
{"name": "Kubernetes", "icon": "k8s"},
|
||||||
|
{"name": "Git", "icon": "git"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"languages": [
|
||||||
|
{"name": "English", "level": "Native"},
|
||||||
|
{"name": "Spanish", "level": "Fluent"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Sample UI Data (data/ui-en.json) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
{
|
||||||
|
"sections": {
|
||||||
|
"summary": "Professional Summary",
|
||||||
|
"experience": "Work Experience",
|
||||||
|
"education": "Education",
|
||||||
|
"skills": "Technical Skills",
|
||||||
|
"languages": "Languages"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"export_pdf": "Export PDF",
|
||||||
|
"toggle_length": "Toggle Length",
|
||||||
|
"toggle_icons": "Toggle Icons",
|
||||||
|
"toggle_theme": "Toggle Theme",
|
||||||
|
"toggle_language": "Switch Language"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "An error occurred",
|
||||||
|
"pdf_generating": "Generating PDF...",
|
||||||
|
"pdf_ready": "PDF is ready for download"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"short_cv": "Short",
|
||||||
|
"long_cv": "Long",
|
||||||
|
"show_icons": "Show Icons",
|
||||||
|
"hide_icons": "Hide Icons"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Validation
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Validation Rules │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
CV Validation:
|
||||||
|
├─ Personal
|
||||||
|
│ ├─ Name: Required, non-empty
|
||||||
|
│ ├─ Title: Required, non-empty
|
||||||
|
│ ├─ Email: Required, valid email format
|
||||||
|
│ └─ Location: Required, non-empty
|
||||||
|
│
|
||||||
|
├─ Experience
|
||||||
|
│ ├─ Company: Required, non-empty
|
||||||
|
│ ├─ Position: Required, non-empty
|
||||||
|
│ ├─ StartDate: Required, valid date (YYYY-MM)
|
||||||
|
│ └─ EndDate: Optional, must be after StartDate if present
|
||||||
|
│
|
||||||
|
├─ Education
|
||||||
|
│ ├─ Institution: Required, non-empty
|
||||||
|
│ ├─ Degree: Required, non-empty
|
||||||
|
│ └─ Field: Required, non-empty
|
||||||
|
│
|
||||||
|
├─ Skills
|
||||||
|
│ ├─ Name: Required, non-empty
|
||||||
|
│ └─ Level: Optional, one of [Basic, Intermediate, Advanced, Expert]
|
||||||
|
│
|
||||||
|
└─ Languages
|
||||||
|
├─ Name: Required, non-empty
|
||||||
|
└─ Level: Required, one of [Native, Fluent, Professional, Intermediate, Basic]
|
||||||
|
|
||||||
|
UI Validation:
|
||||||
|
├─ All section titles: Required, non-empty
|
||||||
|
├─ All button labels: Required, non-empty
|
||||||
|
└─ All messages: Required, non-empty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Model Lifecycle │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Application Start
|
||||||
|
│
|
||||||
|
└─→ Models NOT loaded (lazy loading)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Request Arrives (lang=es)
|
||||||
|
│
|
||||||
|
├─→ Handler calls LoadCV("es")
|
||||||
|
│ ├─ Check cache (if caching enabled)
|
||||||
|
│ ├─ Read data/cv-es.json
|
||||||
|
│ ├─ Parse JSON → CV struct
|
||||||
|
│ ├─ Validate struct
|
||||||
|
│ └─ Return *CV
|
||||||
|
│
|
||||||
|
├─→ Handler calls LoadUI("es")
|
||||||
|
│ ├─ Read data/ui-es.json
|
||||||
|
│ ├─ Parse JSON → UI struct
|
||||||
|
│ └─ Return *UI
|
||||||
|
│
|
||||||
|
└─→ Handler processes data
|
||||||
|
├─ Calculate durations
|
||||||
|
├─ Split skills
|
||||||
|
└─ Render template
|
||||||
|
|
||||||
|
Next Request (lang=es)
|
||||||
|
│
|
||||||
|
└─→ Models reloaded (no persistent cache)
|
||||||
|
(Each request loads fresh data for hot reload)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Transformation
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Transformation Pipeline │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
JSON (static)
|
||||||
|
│
|
||||||
|
├─ "start_date": "2020-01"
|
||||||
|
└─ "end_date": ""
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Go Struct (typed)
|
||||||
|
│
|
||||||
|
├─ StartDate: "2020-01"
|
||||||
|
├─ EndDate: ""
|
||||||
|
└─ Duration: "" (empty)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Calculate Duration
|
||||||
|
│
|
||||||
|
├─ Parse dates
|
||||||
|
├─ Calculate difference
|
||||||
|
└─ Format: "3 years 2 months"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Template Data (enriched)
|
||||||
|
│
|
||||||
|
├─ StartDate: "2020-01"
|
||||||
|
├─ EndDate: "Present"
|
||||||
|
└─ Duration: "3 years 2 months"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
HTML (rendered)
|
||||||
|
│
|
||||||
|
└─ <span class="duration">3 years 2 months</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [System Architecture](./01-system-architecture.md) - Overall system design
|
||||||
|
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
|
||||||
|
- [Template Rendering](./07-template-rendering.md) - Template processing
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
# Error Handling Flow Diagram
|
||||||
|
|
||||||
|
## Error Handling Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Handling Architecture │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Error Types:
|
||||||
|
├── Domain Errors Application-level business logic errors
|
||||||
|
├── Validation Errors Input validation failures
|
||||||
|
├── System Errors Infrastructure/system failures
|
||||||
|
└── Panic Recovery Runtime panic handling
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain Error Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ DomainError (internal/handlers/errors.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type DomainError struct { │
|
||||||
|
│ Code ErrorCode // Enum error code │
|
||||||
|
│ Message string // Human-readable message │
|
||||||
|
│ Err error // Underlying error (if any) │
|
||||||
|
│ StatusCode int // HTTP status code │
|
||||||
|
│ Field string // Field that caused error │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ func (e *DomainError) Error() string │
|
||||||
|
│ func (e *DomainError) Unwrap() error │
|
||||||
|
│ func (e *DomainError) WithField(field string) *DomainError │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Codes │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type ErrorCode string │
|
||||||
|
│ │
|
||||||
|
│ const ( │
|
||||||
|
│ // Input Validation (400) │
|
||||||
|
│ ErrCodeInvalidLanguage = "INVALID_LANGUAGE" │
|
||||||
|
│ ErrCodeInvalidLength = "INVALID_LENGTH" │
|
||||||
|
│ ErrCodeInvalidIcons = "INVALID_ICONS" │
|
||||||
|
│ ErrCodeInvalidTheme = "INVALID_THEME" │
|
||||||
|
│ ErrCodeInvalidVersion = "INVALID_VERSION" │
|
||||||
|
│ ErrCodeValidationFailed = "VALIDATION_FAILED" │
|
||||||
|
│ │
|
||||||
|
│ // Resource Errors (404, 500) │
|
||||||
|
│ ErrCodeDataNotFound = "DATA_NOT_FOUND" │
|
||||||
|
│ ErrCodeTemplateNotFound = "TEMPLATE_NOT_FOUND" │
|
||||||
|
│ ErrCodeTemplateError = "TEMPLATE_ERROR" │
|
||||||
|
│ │
|
||||||
|
│ // Processing Errors (500) │
|
||||||
|
│ ErrCodePDFGeneration = "PDF_GENERATION" │
|
||||||
|
│ ErrCodeInternalError = "INTERNAL_ERROR" │
|
||||||
|
│ │
|
||||||
|
│ // Rate Limiting (429) │
|
||||||
|
│ ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED" │
|
||||||
|
│ │
|
||||||
|
│ // Security (403) │
|
||||||
|
│ ErrCodeOriginMismatch = "ORIGIN_MISMATCH" │
|
||||||
|
│ ) │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Flow Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Validation Error
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Validation Error Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Request: GET /?lang=xx
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Handler.Home() │
|
||||||
|
│ (cv_pages.go) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
├─→ lang := r.URL.Query().Get("lang")
|
||||||
|
│ // lang = "xx"
|
||||||
|
│
|
||||||
|
├─→ err := validateLanguage(lang)
|
||||||
|
│ // "xx" not in ["en", "es"]
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ validateLanguage(lang) │
|
||||||
|
│ (cv_helpers.go) │
|
||||||
|
│ │
|
||||||
|
│ if lang != "en" && lang != "es" { │
|
||||||
|
│ return InvalidLanguageError(lang) │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ InvalidLanguageError(lang) │
|
||||||
|
│ (errors.go) │
|
||||||
|
│ │
|
||||||
|
│ return NewDomainError( │
|
||||||
|
│ ErrCodeInvalidLanguage, │
|
||||||
|
│ fmt.Sprintf("Unsupported language: %s", lang), │
|
||||||
|
│ http.StatusBadRequest, │
|
||||||
|
│ ).WithField("lang") │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler receives error │
|
||||||
|
│ │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ h.HandleError(w, r, err) │
|
||||||
|
│ return │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ HandleError(w, r, err) │
|
||||||
|
│ (errors.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Cast to DomainError │
|
||||||
|
│ domErr, ok := err.(*DomainError) │
|
||||||
|
│ │
|
||||||
|
│ 2. Log error │
|
||||||
|
│ log.Printf("[ERROR] %s: %s", domErr.Code, domErr.Message) │
|
||||||
|
│ │
|
||||||
|
│ 3. Build response │
|
||||||
|
│ response := NewErrorResponse( │
|
||||||
|
│ string(domErr.Code), │
|
||||||
|
│ domErr.Message, │
|
||||||
|
│ ) │
|
||||||
|
│ response.Error.Field = domErr.Field │
|
||||||
|
│ │
|
||||||
|
│ 4. Send JSON error │
|
||||||
|
│ w.Header().Set("Content-Type", "application/json") │
|
||||||
|
│ w.WriteHeader(domErr.StatusCode) │
|
||||||
|
│ json.NewEncoder(w).Encode(response) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Client Response │
|
||||||
|
│ │
|
||||||
|
│ HTTP/1.1 400 Bad Request │
|
||||||
|
│ Content-Type: application/json │
|
||||||
|
│ │
|
||||||
|
│ { │
|
||||||
|
│ "success": false, │
|
||||||
|
│ "error": { │
|
||||||
|
│ "code": "INVALID_LANGUAGE", │
|
||||||
|
│ "message": "Unsupported language: xx (use 'en' or 'es')", │
|
||||||
|
│ "field": "lang" │
|
||||||
|
│ } │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Data Loading Error
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Loading Error Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Handler calls LoadCV("es")
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ cvmodel.LoadCV(lang) │
|
||||||
|
│ (internal/models/cv/cv.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Build file path │
|
||||||
|
│ filePath := fmt.Sprintf("data/cv-%s.json", lang) │
|
||||||
|
│ │
|
||||||
|
│ 2. Read file │
|
||||||
|
│ data, err := os.ReadFile(filePath) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ return nil, fmt.Errorf("failed to read CV: %w", err) │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 3. Parse JSON │
|
||||||
|
│ var cv CV │
|
||||||
|
│ err = json.Unmarshal(data, &cv) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ return nil, fmt.Errorf("failed to parse CV: %w", err) │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 4. Validate │
|
||||||
|
│ if err := cv.Validate(); err != nil { │
|
||||||
|
│ return nil, fmt.Errorf("invalid CV data: %w", err) │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 5. Return │
|
||||||
|
│ return &cv, nil │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ Error Case: File not found
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler receives error │
|
||||||
|
│ │
|
||||||
|
│ cv, err := cvmodel.LoadCV(lang) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ // Wrap in DomainError │
|
||||||
|
│ domErr := DataNotFoundError("CV", lang) │
|
||||||
|
│ domErr.Err = err │
|
||||||
|
│ h.HandleError(w, r, domErr) │
|
||||||
|
│ return │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Client Response │
|
||||||
|
│ │
|
||||||
|
│ HTTP/1.1 500 Internal Server Error │
|
||||||
|
│ Content-Type: application/json │
|
||||||
|
│ │
|
||||||
|
│ { │
|
||||||
|
│ "success": false, │
|
||||||
|
│ "error": { │
|
||||||
|
│ "code": "DATA_NOT_FOUND", │
|
||||||
|
│ "message": "CV data not found for language: es" │
|
||||||
|
│ } │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: PDF Generation Error
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Generation Error Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Handler calls GeneratePDF()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pdf.GeneratePDF(htmlContent, options) │
|
||||||
|
│ (internal/pdf/generator.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Create context │
|
||||||
|
│ ctx, cancel := chromedp.NewContext(...) │
|
||||||
|
│ defer cancel() │
|
||||||
|
│ │
|
||||||
|
│ 2. Launch Chrome │
|
||||||
|
│ if err := chromedp.Run(ctx, ...); err != nil { │
|
||||||
|
│ return nil, fmt.Errorf("chrome launch: %w", err) │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 3. Navigate and render │
|
||||||
|
│ err := chromedp.Run(ctx, │
|
||||||
|
│ chromedp.Navigate(dataURL), │
|
||||||
|
│ chromedp.WaitReady("body"), │
|
||||||
|
│ chromedp.PrintToPDF(&pdfBytes), │
|
||||||
|
│ ) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ return nil, fmt.Errorf("PDF generation: %w", err) │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 4. Return PDF bytes │
|
||||||
|
│ return pdfBytes, nil │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ Error Case: Chrome failed
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler.ExportPDF receives error │
|
||||||
|
│ (cv_pdf.go) │
|
||||||
|
│ │
|
||||||
|
│ pdfBytes, err := pdf.GeneratePDF(htmlContent, opts) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ domErr := PDFGenerationError(err) │
|
||||||
|
│ h.HandleError(w, r, domErr) │
|
||||||
|
│ return │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Client Response │
|
||||||
|
│ │
|
||||||
|
│ HTTP/1.1 500 Internal Server Error │
|
||||||
|
│ Content-Type: application/json │
|
||||||
|
│ │
|
||||||
|
│ { │
|
||||||
|
│ "success": false, │
|
||||||
|
│ "error": { │
|
||||||
|
│ "code": "PDF_GENERATION", │
|
||||||
|
│ "message": "Failed to generate PDF. Please try again." │
|
||||||
|
│ } │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Panic Recovery
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Panic Recovery Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Request enters system
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Recovery Middleware │
|
||||||
|
│ (internal/middleware/recovery.go) │
|
||||||
|
│ │
|
||||||
|
│ func Recovery(next http.Handler) http.Handler { │
|
||||||
|
│ return http.HandlerFunc(func(w, r) { │
|
||||||
|
│ defer func() { │
|
||||||
|
│ if err := recover(); err != nil { │
|
||||||
|
│ // Capture panic │
|
||||||
|
│ stack := debug.Stack() │
|
||||||
|
│ │
|
||||||
|
│ // Log with stack trace │
|
||||||
|
│ log.Printf("PANIC: %v\n%s", err, stack) │
|
||||||
|
│ │
|
||||||
|
│ // Send error response │
|
||||||
|
│ http.Error(w, │
|
||||||
|
│ "Internal Server Error", │
|
||||||
|
│ http.StatusInternalServerError) │
|
||||||
|
│ } │
|
||||||
|
│ }() │
|
||||||
|
│ │
|
||||||
|
│ // Continue to next handler │
|
||||||
|
│ next.ServeHTTP(w, r) │
|
||||||
|
│ }) │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ Normal flow: no panic
|
||||||
|
├──────────────────────────────────┐
|
||||||
|
▼ │ Panic occurs
|
||||||
|
Handler executes ▼
|
||||||
|
│ ┌─────────────────────────────────┐
|
||||||
|
│ │ panic("something went wrong") │
|
||||||
|
│ └─────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ ┌─────────────────────────────────┐
|
||||||
|
│ │ defer recover() catches it │
|
||||||
|
│ │ ├─ Get stack trace │
|
||||||
|
│ │ ├─ Log error + stack │
|
||||||
|
│ │ └─ Send 500 response │
|
||||||
|
│ └─────────────────────────────────┘
|
||||||
|
▼ │
|
||||||
|
Response sent ▼
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Client receives 500 │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Response Formats
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Response Formats │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Standard API Error (JSON):
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "INVALID_LANGUAGE",
|
||||||
|
"message": "Unsupported language: xx (use 'en' or 'es')",
|
||||||
|
"field": "lang"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Validation Error with Multiple Fields:
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_FAILED",
|
||||||
|
"message": "Request validation failed",
|
||||||
|
"fields": {
|
||||||
|
"lang": "Invalid language",
|
||||||
|
"length": "Invalid length"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Internal Error (Generic):
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "INTERNAL_ERROR",
|
||||||
|
"message": "An unexpected error occurred. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HTML Error Page (for page requests):
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Error</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Oops! Something went wrong</h1>
|
||||||
|
<p>We're sorry, but we couldn't process your request.</p>
|
||||||
|
<p>Error: INVALID_LANGUAGE</p>
|
||||||
|
<a href="/">Go back home</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Logging
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Logging │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Log Format:
|
||||||
|
[ERROR] <ERROR_CODE>: <message>
|
||||||
|
[ERROR] Additional context: <details>
|
||||||
|
[ERROR] Stack trace (if panic):
|
||||||
|
<stack trace lines>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
[ERROR] INVALID_LANGUAGE: Unsupported language: xx (use 'en' or 'es')
|
||||||
|
[ERROR] Field: lang
|
||||||
|
|
||||||
|
[ERROR] PDF_GENERATION: Failed to generate PDF
|
||||||
|
[ERROR] Underlying error: chrome launch failed: context deadline exceeded
|
||||||
|
|
||||||
|
[ERROR] PANIC: runtime error: invalid memory address
|
||||||
|
[ERROR] Stack trace:
|
||||||
|
goroutine 23 [running]:
|
||||||
|
main.(*CVHandler).Home(...)
|
||||||
|
/app/internal/handlers/cv_pages.go:42
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Best Practices
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Error Handling Best Practices │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
1. USE TYPED ERRORS
|
||||||
|
✓ return InvalidLanguageError(lang)
|
||||||
|
✗ return errors.New("invalid language")
|
||||||
|
|
||||||
|
2. WRAP ERRORS WITH CONTEXT
|
||||||
|
✓ return fmt.Errorf("failed to load CV: %w", err)
|
||||||
|
✗ return err
|
||||||
|
|
||||||
|
3. LOG BEFORE RESPONDING
|
||||||
|
✓ log.Printf("[ERROR] %s", err)
|
||||||
|
h.HandleError(w, r, err)
|
||||||
|
✗ h.HandleError(w, r, err) // No logging
|
||||||
|
|
||||||
|
4. USE APPROPRIATE STATUS CODES
|
||||||
|
✓ 400 for validation errors
|
||||||
|
404 for not found
|
||||||
|
429 for rate limiting
|
||||||
|
500 for server errors
|
||||||
|
✗ Always returning 500
|
||||||
|
|
||||||
|
5. DON'T LEAK INTERNAL DETAILS
|
||||||
|
✓ "Failed to generate PDF. Please try again."
|
||||||
|
✗ "chromedp: chrome crashed at line 42 in generator.go"
|
||||||
|
|
||||||
|
6. PROVIDE ACTIONABLE MESSAGES
|
||||||
|
✓ "Unsupported language: xx (use 'en' or 'es')"
|
||||||
|
✗ "Invalid input"
|
||||||
|
|
||||||
|
7. USE RECOVERY MIDDLEWARE
|
||||||
|
✓ Catch all panics at middleware level
|
||||||
|
✗ Let panics crash the server
|
||||||
|
|
||||||
|
8. INCLUDE FIELD INFORMATION
|
||||||
|
✓ error.WithField("lang")
|
||||||
|
✗ Generic error without field context
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
|
||||||
|
- [Middleware Chain](./03-middleware-chain.md) - Middleware execution
|
||||||
|
- [Handler Organization](./04-handler-organization.md) - Handler structure
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
# Template Rendering Diagram
|
||||||
|
|
||||||
|
## Template System Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template System Architecture │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
internal/templates/
|
||||||
|
├── manager.go Template manager (caching, rendering)
|
||||||
|
└── functions.go Custom template functions
|
||||||
|
|
||||||
|
templates/
|
||||||
|
├── index.html Main page template
|
||||||
|
├── partials/ Reusable components
|
||||||
|
│ ├── header.html
|
||||||
|
│ ├── footer.html
|
||||||
|
│ ├── cv_content.html
|
||||||
|
│ ├── experience.html
|
||||||
|
│ ├── education.html
|
||||||
|
│ ├── skills.html
|
||||||
|
│ └── languages.html
|
||||||
|
└── layouts/ Layout templates
|
||||||
|
└── base.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Manager
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Manager (internal/templates/manager.go) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ type Manager struct { │
|
||||||
|
│ templates map[string]*template.Template │
|
||||||
|
│ config *config.TemplateConfig │
|
||||||
|
│ mu sync.RWMutex // Thread-safe access │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ type TemplateConfig struct { │
|
||||||
|
│ Dir string // templates/ │
|
||||||
|
│ PartialsDir string // templates/partials/ │
|
||||||
|
│ HotReload bool // Reload on every render │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ Methods: │
|
||||||
|
│ ├─ NewManager(config) (*Manager, error) │
|
||||||
|
│ │ └─→ Initialize and load all templates │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ Render(w, name, data) error │
|
||||||
|
│ │ └─→ Execute template with data │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ loadTemplates() error │
|
||||||
|
│ │ └─→ Parse and cache all templates │
|
||||||
|
│ │ │
|
||||||
|
│ └─ reloadIfNeeded() error │
|
||||||
|
│ └─→ Reload templates if hot reload enabled │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Loading Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Loading Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Application Start
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ NewManager(config) │
|
||||||
|
│ (internal/templates/manager.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Create manager │
|
||||||
|
│ m := &Manager{ │
|
||||||
|
│ templates: make(map[string]*template.Template), │
|
||||||
|
│ config: config, │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 2. Load all templates │
|
||||||
|
│ if err := m.loadTemplates(); err != nil { │
|
||||||
|
│ return nil, err │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 3. Return manager │
|
||||||
|
│ return m, nil │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ loadTemplates() │
|
||||||
|
│ │
|
||||||
|
│ 1. Scan template directory │
|
||||||
|
│ files, err := filepath.Glob(config.Dir + "/*.html") │
|
||||||
|
│ │
|
||||||
|
│ 2. For each template file: │
|
||||||
|
│ ├─ Create new template │
|
||||||
|
│ │ tmpl := template.New(name) │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ Add custom functions │
|
||||||
|
│ │ tmpl.Funcs(customFunctions()) │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ Parse main template │
|
||||||
|
│ │ tmpl.ParseFiles(file) │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ Parse partials │
|
||||||
|
│ │ tmpl.ParseGlob(config.PartialsDir + "/*.html") │
|
||||||
|
│ │ │
|
||||||
|
│ └─ Cache template │
|
||||||
|
│ m.templates[name] = tmpl │
|
||||||
|
│ │
|
||||||
|
│ 3. Log loaded templates │
|
||||||
|
│ log.Printf("Loaded %d templates", len(m.templates)) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Rendering Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Rendering Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Handler calls Render()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Manager.Render(w, "index.html", data) │
|
||||||
|
│ (internal/templates/manager.go) │
|
||||||
|
│ │
|
||||||
|
│ 1. Lock for reading │
|
||||||
|
│ m.mu.RLock() │
|
||||||
|
│ defer m.mu.RUnlock() │
|
||||||
|
│ │
|
||||||
|
│ 2. Hot reload check │
|
||||||
|
│ if m.config.HotReload { │
|
||||||
|
│ m.mu.RUnlock() │
|
||||||
|
│ m.mu.Lock() │
|
||||||
|
│ m.loadTemplates() // Reload all templates │
|
||||||
|
│ m.mu.Unlock() │
|
||||||
|
│ m.mu.RLock() │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 3. Get template from cache │
|
||||||
|
│ tmpl, ok := m.templates[name] │
|
||||||
|
│ if !ok { │
|
||||||
|
│ return fmt.Errorf("template not found: %s", name) │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 4. Execute template │
|
||||||
|
│ err := tmpl.Execute(w, data) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ return fmt.Errorf("template execution: %w", err) │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ 5. Return │
|
||||||
|
│ return nil │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Execution │
|
||||||
|
│ │
|
||||||
|
│ 1. Parse template directives │
|
||||||
|
│ {{.CV.Personal.Name}} │
|
||||||
|
│ {{range .CV.Experience}}...{{end}} │
|
||||||
|
│ {{template "partials/header.html" .}} │
|
||||||
|
│ │
|
||||||
|
│ 2. Execute custom functions │
|
||||||
|
│ {{formatDate .StartDate}} │
|
||||||
|
│ {{join .Highlights ", "}} │
|
||||||
|
│ {{lower .CVLanguage}} │
|
||||||
|
│ │
|
||||||
|
│ 3. Include partials │
|
||||||
|
│ {{template "partials/cv_content.html" .}} │
|
||||||
|
│ {{template "partials/experience.html" .}} │
|
||||||
|
│ │
|
||||||
|
│ 4. Generate HTML │
|
||||||
|
│ Write to http.ResponseWriter │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Hierarchy │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
index.html (Main Template)
|
||||||
|
│
|
||||||
|
├─→ {{template "partials/header.html" .}}
|
||||||
|
│ └─→ Navigation, language toggle, theme toggle
|
||||||
|
│
|
||||||
|
├─→ {{template "partials/cv_content.html" .}}
|
||||||
|
│ │
|
||||||
|
│ ├─→ {{template "partials/experience.html" .}}
|
||||||
|
│ │ └─→ {{range .CV.Experience}}
|
||||||
|
│ │ ├─ Company, position, dates
|
||||||
|
│ │ ├─ {{.Duration}} (calculated)
|
||||||
|
│ │ └─ {{range .Highlights}}
|
||||||
|
│ │
|
||||||
|
│ ├─→ {{template "partials/education.html" .}}
|
||||||
|
│ │ └─→ {{range .CV.Education}}
|
||||||
|
│ │ ├─ Institution, degree, field
|
||||||
|
│ │ └─ Dates, GPA, honors
|
||||||
|
│ │
|
||||||
|
│ ├─→ {{template "partials/skills.html" .}}
|
||||||
|
│ │ └─→ {{range .SkillsColumns}}
|
||||||
|
│ │ └─ {{range .}}
|
||||||
|
│ │ ├─ Skill name
|
||||||
|
│ │ ├─ Level badge
|
||||||
|
│ │ └─ Icon (if enabled)
|
||||||
|
│ │
|
||||||
|
│ └─→ {{template "partials/languages.html" .}}
|
||||||
|
│ └─→ {{range .CV.Languages}}
|
||||||
|
│ ├─ Language name
|
||||||
|
│ └─ Proficiency level
|
||||||
|
│
|
||||||
|
└─→ {{template "partials/footer.html" .}}
|
||||||
|
└─→ PDF export button, copyright
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Data Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Data Structure │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Data passed to templates:
|
||||||
|
|
||||||
|
map[string]interface{}{
|
||||||
|
// CV Data
|
||||||
|
"CV": &cvmodel.CV{
|
||||||
|
Personal: cvmodel.Personal{
|
||||||
|
Name: "John Doe",
|
||||||
|
Title: "Senior Software Engineer",
|
||||||
|
Email: "john@example.com",
|
||||||
|
Location: "San Francisco, CA",
|
||||||
|
},
|
||||||
|
Experience: []cvmodel.Experience{
|
||||||
|
{
|
||||||
|
Company: "Tech Corp",
|
||||||
|
Position: "Senior Engineer",
|
||||||
|
StartDate: "2020-01",
|
||||||
|
EndDate: "",
|
||||||
|
Current: true,
|
||||||
|
Duration: "3 years 2 months", // Calculated
|
||||||
|
Highlights: []string{...},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Education: []cvmodel.Education{...},
|
||||||
|
Skills: cvmodel.Skills{...},
|
||||||
|
Languages: []cvmodel.Language{...},
|
||||||
|
},
|
||||||
|
|
||||||
|
// UI Strings
|
||||||
|
"UI": &uimodel.UI{
|
||||||
|
Sections: uimodel.Sections{
|
||||||
|
Summary: "Professional Summary",
|
||||||
|
Experience: "Work Experience",
|
||||||
|
Education: "Education",
|
||||||
|
Skills: "Technical Skills",
|
||||||
|
Languages: "Languages",
|
||||||
|
},
|
||||||
|
Buttons: uimodel.Buttons{...},
|
||||||
|
Messages: uimodel.Messages{...},
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Preferences
|
||||||
|
"Preferences": &middleware.Preferences{
|
||||||
|
CVLength: "long",
|
||||||
|
CVIcons: "show",
|
||||||
|
CVLanguage: "es",
|
||||||
|
CVTheme: "default",
|
||||||
|
ColorTheme: "light",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Processed Data
|
||||||
|
"SkillsColumns": [][]cvmodel.Skill{
|
||||||
|
[]cvmodel.Skill{...}, // Column 1
|
||||||
|
[]cvmodel.Skill{...}, // Column 2
|
||||||
|
[]cvmodel.Skill{...}, // Column 3
|
||||||
|
},
|
||||||
|
|
||||||
|
// SEO Metadata
|
||||||
|
"PageTitle": "John Doe - Senior Software Engineer",
|
||||||
|
"MetaDescription": "Professional CV of John Doe...",
|
||||||
|
"CanonicalURL": "http://localhost:8080/",
|
||||||
|
"OGImage": "http://localhost:8080/static/images/og-image.png",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Template Functions
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Custom Template Functions │
|
||||||
|
│ (internal/templates/functions.go) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
template.FuncMap{
|
||||||
|
// String manipulation
|
||||||
|
"lower": strings.ToLower,
|
||||||
|
"upper": strings.ToUpper,
|
||||||
|
"title": strings.Title,
|
||||||
|
|
||||||
|
// Date formatting
|
||||||
|
"formatDate": func(date string) string {
|
||||||
|
if date == "" {
|
||||||
|
return "Present"
|
||||||
|
}
|
||||||
|
t, _ := time.Parse("2006-01", date)
|
||||||
|
return t.Format("Jan 2006")
|
||||||
|
},
|
||||||
|
|
||||||
|
// Array operations
|
||||||
|
"join": strings.Join,
|
||||||
|
"split": strings.Split,
|
||||||
|
|
||||||
|
// Math
|
||||||
|
"add": func(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
},
|
||||||
|
"multiply": func(a, b int) int {
|
||||||
|
return a * b
|
||||||
|
},
|
||||||
|
|
||||||
|
// Conditional helpers
|
||||||
|
"eq": func(a, b interface{}) bool {
|
||||||
|
return a == b
|
||||||
|
},
|
||||||
|
"ne": func(a, b interface{}) bool {
|
||||||
|
return a != b
|
||||||
|
},
|
||||||
|
|
||||||
|
// HTML safety
|
||||||
|
"safe": func(s string) template.HTML {
|
||||||
|
return template.HTML(s)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Usage in templates:
|
||||||
|
|
||||||
|
{{formatDate .StartDate}}
|
||||||
|
// "2020-01" → "Jan 2020"
|
||||||
|
|
||||||
|
{{join .Highlights ", "}}
|
||||||
|
// ["foo", "bar"] → "foo, bar"
|
||||||
|
|
||||||
|
{{if eq .CVLength "long"}}
|
||||||
|
<!-- Show long content -->
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{.Description | safe}}
|
||||||
|
// Render HTML without escaping
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Conditionals
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Conditionals │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Show/Hide based on CV length:
|
||||||
|
{{if eq .Preferences.CVLength "long"}}
|
||||||
|
<!-- Show full details -->
|
||||||
|
<div class="experience-highlights">
|
||||||
|
{{range .Highlights}}
|
||||||
|
<li>{{.}}</li>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
Show/Hide based on icons preference:
|
||||||
|
{{if eq .Preferences.CVIcons "show"}}
|
||||||
|
<i class="icon-{{.Icon}}"></i>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
Conditional classes:
|
||||||
|
<div class="cv-section {{if eq .Preferences.CVTheme "minimal"}}minimal{{end}}">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Language-specific content:
|
||||||
|
{{if eq .Preferences.CVLanguage "es"}}
|
||||||
|
<span>Experiencia Profesional</span>
|
||||||
|
{{else}}
|
||||||
|
<span>Professional Experience</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
Current vs. past experience:
|
||||||
|
{{if .Current}}
|
||||||
|
<span class="badge current">Present</span>
|
||||||
|
{{else}}
|
||||||
|
<span>{{formatDate .EndDate}}</span>
|
||||||
|
{{end}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Performance
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Performance │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Performance Characteristics:
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Operation Time Notes │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Template Loading ~50ms On app start │
|
||||||
|
│ ├─ Parse templates ~40ms Compile Go templates│
|
||||||
|
│ └─ Cache templates ~10ms Store in map │
|
||||||
|
│ │
|
||||||
|
│ Template Rendering ~45ms Per request │
|
||||||
|
│ ├─ Template lookup ~10ns Map access │
|
||||||
|
│ ├─ Template execute ~40ms Main cost │
|
||||||
|
│ ├─ Partial includes ~5ms Include partials │
|
||||||
|
│ └─ Function calls ~100μs Custom functions │
|
||||||
|
│ │
|
||||||
|
│ Hot Reload ~50ms If enabled │
|
||||||
|
│ └─ Reload all ~50ms Parse again │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Optimization Strategies:
|
||||||
|
1. Template Caching
|
||||||
|
└─→ Pre-compile templates at startup
|
||||||
|
Serve from memory cache
|
||||||
|
|
||||||
|
2. Hot Reload (Development Only)
|
||||||
|
└─→ Reload on every request for dev
|
||||||
|
Disable in production for speed
|
||||||
|
|
||||||
|
3. Minimize Partials
|
||||||
|
└─→ Balance reusability vs. overhead
|
||||||
|
Each partial adds ~1ms
|
||||||
|
|
||||||
|
4. Pre-calculate Data
|
||||||
|
└─→ Calculate durations in handler
|
||||||
|
Split skills before rendering
|
||||||
|
|
||||||
|
5. Use Buffer Pool
|
||||||
|
└─→ Reuse buffers for rendering
|
||||||
|
Reduce allocations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Error Handling
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Template Error Handling │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Error Types:
|
||||||
|
|
||||||
|
1. Template Not Found
|
||||||
|
Error: template "foo.html" not found
|
||||||
|
Cause: Template doesn't exist in cache
|
||||||
|
Fix: Create template file, reload
|
||||||
|
|
||||||
|
2. Parse Error
|
||||||
|
Error: template: index.html:42: unexpected "}"
|
||||||
|
Cause: Syntax error in template
|
||||||
|
Fix: Check template syntax
|
||||||
|
|
||||||
|
3. Execution Error
|
||||||
|
Error: template: executing "index.html": map has no entry for key "Foo"
|
||||||
|
Cause: Missing data in template data map
|
||||||
|
Fix: Ensure all required data passed
|
||||||
|
|
||||||
|
4. Function Error
|
||||||
|
Error: template: function "unknownFunc" not defined
|
||||||
|
Cause: Custom function not registered
|
||||||
|
Fix: Register function in FuncMap
|
||||||
|
|
||||||
|
Error Flow:
|
||||||
|
|
||||||
|
Template Error
|
||||||
|
│
|
||||||
|
├─→ Logged with stack trace
|
||||||
|
│ log.Printf("[ERROR] Template: %v", err)
|
||||||
|
│
|
||||||
|
├─→ Wrapped in DomainError
|
||||||
|
│ TemplateError(err)
|
||||||
|
│
|
||||||
|
└─→ Sent as 500 response
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "TEMPLATE_ERROR",
|
||||||
|
"message": "Failed to render page"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hot Reload Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Hot Reload Flow │
|
||||||
|
│ (Development Mode) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Developer edits template
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Next request arrives
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Render() called │
|
||||||
|
│ │
|
||||||
|
│ if m.config.HotReload { │
|
||||||
|
│ // Reload all templates │
|
||||||
|
│ m.mu.Lock() │
|
||||||
|
│ m.loadTemplates() │
|
||||||
|
│ m.mu.Unlock() │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ // Use fresh templates │
|
||||||
|
│ tmpl := m.templates[name] │
|
||||||
|
│ tmpl.Execute(w, data) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Page rendered with updated template
|
||||||
|
(No server restart needed)
|
||||||
|
|
||||||
|
⚠️ Hot reload disabled in production for performance
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [System Architecture](./01-system-architecture.md) - Overall system design
|
||||||
|
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
|
||||||
|
- [Handler Organization](./04-handler-organization.md) - Handler structure
|
||||||
|
- [Data Models](./05-data-models.md) - Data structures
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
# PDF Generation Diagram
|
||||||
|
|
||||||
|
## PDF Export Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Export Architecture │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Client (Browser)
|
||||||
|
│
|
||||||
|
├─→ User clicks "Export PDF"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Modal with options │
|
||||||
|
│ ├─ Language (en/es) │
|
||||||
|
│ ├─ Length (short/long) │
|
||||||
|
│ ├─ Icons (show/hide) │
|
||||||
|
│ └─ Version (with/clean)│
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /export/pdf
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Route Middleware │
|
||||||
|
│ ├─ OriginChecker │
|
||||||
|
│ └─ RateLimiter │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ CVHandler.ExportPDF() │
|
||||||
|
│ (cv_pdf.go) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ PDF Generator │
|
||||||
|
│ (internal/pdf/) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Chromedp │
|
||||||
|
│ (Headless Chrome) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
PDF Response
|
||||||
|
```
|
||||||
|
|
||||||
|
## PDF Generation Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Generation Flow │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
1. REQUEST VALIDATION
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Handler.ExportPDF(w, r) │
|
||||||
|
│ (internal/handlers/cv_pdf.go) │
|
||||||
|
│ │
|
||||||
|
│ // Parse JSON request │
|
||||||
|
│ var req PDFExportRequest │
|
||||||
|
│ err := json.NewDecoder(r.Body).Decode(&req) │
|
||||||
|
│ │
|
||||||
|
│ // Validate fields │
|
||||||
|
│ if req.Lang != "en" && req.Lang != "es" { │
|
||||||
|
│ return InvalidLanguageError(req.Lang) │
|
||||||
|
│ } │
|
||||||
|
│ if req.Length != "short" && req.Length != "long" { │
|
||||||
|
│ return InvalidLengthError(req.Length) │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
2. HTML GENERATION
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ // Build template data │
|
||||||
|
│ data := map[string]interface{}{ │
|
||||||
|
│ "CV": cv, │
|
||||||
|
│ "UI": ui, │
|
||||||
|
│ "Preferences": &middleware.Preferences{ │
|
||||||
|
│ CVLength: req.Length, │
|
||||||
|
│ CVIcons: req.Icons, │
|
||||||
|
│ CVLanguage: req.Lang, │
|
||||||
|
│ }, │
|
||||||
|
│ "SkillsColumns": skillColumns, │
|
||||||
|
│ "IsPDF": true, // PDF-specific flag │
|
||||||
|
│ } │
|
||||||
|
│ │
|
||||||
|
│ // Render to buffer │
|
||||||
|
│ var buf bytes.Buffer │
|
||||||
|
│ err := h.tmpl.Render(&buf, "index.html", data) │
|
||||||
|
│ htmlContent := buf.String() │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
3. PDF OPTIONS
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ opts := pdf.Options{ │
|
||||||
|
│ PaperSize: pdf.A4, │
|
||||||
|
│ Orientation: pdf.Portrait, │
|
||||||
|
│ MarginTop: "1cm", │
|
||||||
|
│ MarginRight: "1cm", │
|
||||||
|
│ MarginBottom: "1cm", │
|
||||||
|
│ MarginLeft: "1cm", │
|
||||||
|
│ PrintBackground: true, // Include colors │
|
||||||
|
│ Scale: 1.0, │
|
||||||
|
│ Landscape: false, │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
4. PDF GENERATION
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ pdfBytes, err := pdf.GeneratePDF(htmlContent, opts) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ return PDFGenerationError(err) │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
5. RESPONSE
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ // Build filename │
|
||||||
|
│ filename := fmt.Sprintf("CV-%s-%s.pdf", │
|
||||||
|
│ cv.Personal.Name, req.Lang) │
|
||||||
|
│ filename = strings.ReplaceAll(filename, " ", "-") │
|
||||||
|
│ │
|
||||||
|
│ // Set headers │
|
||||||
|
│ w.Header().Set("Content-Type", "application/pdf") │
|
||||||
|
│ w.Header().Set("Content-Disposition", │
|
||||||
|
│ fmt.Sprintf("attachment; filename=%s", filename)) │
|
||||||
|
│ w.Header().Set("Content-Length", │
|
||||||
|
│ fmt.Sprintf("%d", len(pdfBytes))) │
|
||||||
|
│ │
|
||||||
|
│ // Send PDF │
|
||||||
|
│ w.WriteHeader(http.StatusOK) │
|
||||||
|
│ w.Write(pdfBytes) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chromedp PDF Generation
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Chromedp PDF Generation (internal/pdf/generator.go) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
func GeneratePDF(htmlContent string, opts Options) ([]byte, error) {
|
||||||
|
|
||||||
|
1. CREATE CONTEXT
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ // Allocate context │
|
||||||
|
│ ctx, cancel := chromedp.NewContext( │
|
||||||
|
│ context.Background(), │
|
||||||
|
│ chromedp.WithLogf(log.Printf), │
|
||||||
|
│ ) │
|
||||||
|
│ defer cancel() │
|
||||||
|
│ │
|
||||||
|
│ // Set timeout │
|
||||||
|
│ ctx, cancel = context.WithTimeout(ctx, 30*time.Second) │
|
||||||
|
│ defer cancel() │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
2. PREPARE HTML
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ // Wrap HTML in data URL │
|
||||||
|
│ dataURL := fmt.Sprintf( │
|
||||||
|
│ "data:text/html;base64,%s", │
|
||||||
|
│ base64.StdEncoding.EncodeToString( │
|
||||||
|
│ []byte(htmlContent), │
|
||||||
|
│ ), │
|
||||||
|
│ ) │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
3. LAUNCH CHROME
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ // Run Chrome tasks │
|
||||||
|
│ var pdfBytes []byte │
|
||||||
|
│ err := chromedp.Run(ctx, │
|
||||||
|
│ // Navigate to data URL │
|
||||||
|
│ chromedp.Navigate(dataURL), │
|
||||||
|
│ │
|
||||||
|
│ // Wait for body to be ready │
|
||||||
|
│ chromedp.WaitReady("body", chromedp.ByQuery), │
|
||||||
|
│ │
|
||||||
|
│ // Wait for fonts and images │
|
||||||
|
│ chromedp.Sleep(500 * time.Millisecond), │
|
||||||
|
│ │
|
||||||
|
│ // Generate PDF │
|
||||||
|
│ chromedp.ActionFunc(func(ctx context.Context) error { │
|
||||||
|
│ buf, _, err := page.PrintToPDF(). │
|
||||||
|
│ WithPrintBackground(opts.PrintBackground). │
|
||||||
|
│ WithPaperWidth(opts.PaperWidth). │
|
||||||
|
│ WithPaperHeight(opts.PaperHeight). │
|
||||||
|
│ WithMarginTop(opts.MarginTop). │
|
||||||
|
│ WithMarginRight(opts.MarginRight). │
|
||||||
|
│ WithMarginBottom(opts.MarginBottom). │
|
||||||
|
│ WithMarginLeft(opts.MarginLeft). │
|
||||||
|
│ WithScale(opts.Scale). │
|
||||||
|
│ Do(ctx) │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ return err │
|
||||||
|
│ } │
|
||||||
|
│ pdfBytes = buf │
|
||||||
|
│ return nil │
|
||||||
|
│ }), │
|
||||||
|
│ ) │
|
||||||
|
│ │
|
||||||
|
│ if err != nil { │
|
||||||
|
│ return nil, fmt.Errorf("chromedp: %w", err) │
|
||||||
|
│ } │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
4. RETURN PDF
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ return pdfBytes, nil │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PDF-Specific Template Adjustments
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF-Specific Template Adjustments │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
In templates/index.html:
|
||||||
|
|
||||||
|
{{if .IsPDF}}
|
||||||
|
<!-- PDF-specific styles -->
|
||||||
|
<style>
|
||||||
|
/* Hide interactive elements */
|
||||||
|
.toggle-button, .interactive-controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize for print */
|
||||||
|
body {
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better page breaks */
|
||||||
|
.experience-item {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Consistent sizing */
|
||||||
|
.cv-section {
|
||||||
|
margin-bottom: 1.5cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Font optimization */
|
||||||
|
body {
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{else}}
|
||||||
|
<!-- Web-specific styles -->
|
||||||
|
<style>
|
||||||
|
.interactive-controls {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{end}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PDF Request/Response Example
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Request/Response Example │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
REQUEST:
|
||||||
|
POST /export/pdf HTTP/1.1
|
||||||
|
Host: localhost:8080
|
||||||
|
Content-Type: application/json
|
||||||
|
Origin: http://localhost:8080
|
||||||
|
|
||||||
|
{
|
||||||
|
"lang": "es",
|
||||||
|
"length": "long",
|
||||||
|
"icons": "show",
|
||||||
|
"version": "with_skills"
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSE (Success):
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/pdf
|
||||||
|
Content-Disposition: attachment; filename="CV-John-Doe-es.pdf"
|
||||||
|
Content-Length: 245678
|
||||||
|
|
||||||
|
[PDF binary data]
|
||||||
|
|
||||||
|
RESPONSE (Error - Invalid Language):
|
||||||
|
HTTP/1.1 400 Bad Request
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "INVALID_LANGUAGE",
|
||||||
|
"message": "Unsupported language: xx (use 'en' or 'es')",
|
||||||
|
"field": "lang"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSE (Error - Rate Limited):
|
||||||
|
HTTP/1.1 429 Too Many Requests
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "RATE_LIMIT_EXCEEDED",
|
||||||
|
"message": "Too many PDF exports. Please wait a minute."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSE (Error - PDF Generation Failed):
|
||||||
|
HTTP/1.1 500 Internal Server Error
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "PDF_GENERATION",
|
||||||
|
"message": "Failed to generate PDF. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PDF Options Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Options (internal/pdf/options.go) │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
// Paper settings
|
||||||
|
PaperSize PaperSize // A4, Letter, Legal
|
||||||
|
Orientation Orientation // Portrait, Landscape
|
||||||
|
PaperWidth float64 // In inches
|
||||||
|
PaperHeight float64 // In inches
|
||||||
|
|
||||||
|
// Margins
|
||||||
|
MarginTop string // "1cm", "0.5in"
|
||||||
|
MarginRight string
|
||||||
|
MarginBottom string
|
||||||
|
MarginLeft string
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
PrintBackground bool // Include background colors
|
||||||
|
Scale float64 // 0.5 to 2.0
|
||||||
|
Landscape bool // True for landscape
|
||||||
|
|
||||||
|
// Quality
|
||||||
|
PreferCSSPageSize bool
|
||||||
|
DisplayHeaderFooter bool
|
||||||
|
HeaderTemplate string
|
||||||
|
FooterTemplate string
|
||||||
|
}
|
||||||
|
|
||||||
|
Default A4 Options:
|
||||||
|
Options{
|
||||||
|
PaperSize: A4, // 8.27 x 11.69 inches
|
||||||
|
Orientation: Portrait,
|
||||||
|
MarginTop: "1cm",
|
||||||
|
MarginRight: "1cm",
|
||||||
|
MarginBottom: "1cm",
|
||||||
|
MarginLeft: "1cm",
|
||||||
|
PrintBackground: true,
|
||||||
|
Scale: 1.0,
|
||||||
|
Landscape: false,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Rate Limiting for PDF Export │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
RateLimiter Middleware:
|
||||||
|
├─ 3 requests per minute per IP
|
||||||
|
├─ Uses token bucket algorithm
|
||||||
|
└─ Applied only to /export/pdf endpoint
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
type RateLimiter struct {
|
||||||
|
requests map[string]*bucket
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type bucket struct {
|
||||||
|
tokens int
|
||||||
|
lastReset time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Allow(ip string) bool {
|
||||||
|
rl.mu.Lock()
|
||||||
|
defer rl.mu.Unlock()
|
||||||
|
|
||||||
|
bucket := rl.requests[ip]
|
||||||
|
if bucket == nil {
|
||||||
|
bucket = &bucket{
|
||||||
|
tokens: 3,
|
||||||
|
lastReset: time.Now(),
|
||||||
|
}
|
||||||
|
rl.requests[ip] = bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset bucket every minute
|
||||||
|
if time.Since(bucket.lastReset) > time.Minute {
|
||||||
|
bucket.tokens = 3
|
||||||
|
bucket.lastReset = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tokens
|
||||||
|
if bucket.tokens <= 0 {
|
||||||
|
return false // Rate limited
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.tokens--
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
Response when rate limited:
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "RATE_LIMIT_EXCEEDED",
|
||||||
|
"message": "Too many PDF exports. Please wait a minute."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PDF Performance
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Performance │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Timing Breakdown:
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Operation Time % │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Request validation ~1ms 0.1% │
|
||||||
|
│ HTML generation ~50ms 5% │
|
||||||
|
│ Chrome launch ~200ms 20% │
|
||||||
|
│ Page navigation ~100ms 10% │
|
||||||
|
│ Font loading ~50ms 5% │
|
||||||
|
│ PDF rendering ~550ms 55% │
|
||||||
|
│ Response transmission ~50ms 5% │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ TOTAL ~1000ms 100% │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Optimization Strategies:
|
||||||
|
1. Keep Chrome instance warm
|
||||||
|
└─→ Pre-launch Chrome on startup
|
||||||
|
Reuse context for multiple PDFs
|
||||||
|
|
||||||
|
2. Optimize HTML
|
||||||
|
└─→ Inline critical CSS
|
||||||
|
Remove unused styles
|
||||||
|
|
||||||
|
3. Font optimization
|
||||||
|
└─→ Use web-safe fonts
|
||||||
|
Preload font files
|
||||||
|
|
||||||
|
4. Cache templates
|
||||||
|
└─→ Pre-compile templates
|
||||||
|
Reuse parsed templates
|
||||||
|
|
||||||
|
5. Parallel processing
|
||||||
|
└─→ Queue PDF jobs
|
||||||
|
Process multiple concurrently
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Scenarios
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ PDF Error Scenarios │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
1. Chrome Launch Failed
|
||||||
|
Error: chromedp: failed to allocate context
|
||||||
|
Cause: Chrome not installed or crashed
|
||||||
|
Recovery: Log error, return 500, suggest retry
|
||||||
|
|
||||||
|
2. Timeout
|
||||||
|
Error: context deadline exceeded
|
||||||
|
Cause: PDF generation took > 30 seconds
|
||||||
|
Recovery: Cancel operation, return timeout error
|
||||||
|
|
||||||
|
3. Memory Limit
|
||||||
|
Error: out of memory
|
||||||
|
Cause: Too many concurrent PDF generations
|
||||||
|
Recovery: Rate limiting, queue system
|
||||||
|
|
||||||
|
4. Template Error
|
||||||
|
Error: template execution failed
|
||||||
|
Cause: Missing data or invalid template
|
||||||
|
Recovery: Fix template, ensure all data present
|
||||||
|
|
||||||
|
5. Navigation Error
|
||||||
|
Error: navigation failed
|
||||||
|
Cause: Invalid HTML or data URL too large
|
||||||
|
Recovery: Check HTML validity, reduce size
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Diagrams
|
||||||
|
|
||||||
|
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
|
||||||
|
- [Handler Organization](./04-handler-organization.md) - Handler structure
|
||||||
|
- [Error Handling Flow](./06-error-handling-flow.md) - Error handling
|
||||||
|
- [Template Rendering](./07-template-rendering.md) - Template system
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Architecture Diagrams
|
||||||
|
|
||||||
|
Visual representations of the CV website architecture, data flow, and component relationships.
|
||||||
|
|
||||||
|
## Available Diagrams
|
||||||
|
|
||||||
|
1. [System Architecture](./01-system-architecture.md) - Overall system design
|
||||||
|
2. [Request Flow](./02-request-flow.md) - HTTP request lifecycle
|
||||||
|
3. [Middleware Chain](./03-middleware-chain.md) - Middleware execution order
|
||||||
|
4. [Handler Organization](./04-handler-organization.md) - Handler file structure
|
||||||
|
5. [Data Models](./05-data-models.md) - CV and UI data structures
|
||||||
|
6. [Error Handling Flow](./06-error-handling-flow.md) - Error propagation and handling
|
||||||
|
7. [Template Rendering](./07-template-rendering.md) - Template compilation and rendering
|
||||||
|
8. [PDF Generation](./08-pdf-generation.md) - PDF export process
|
||||||
|
|
||||||
|
## Diagram Format
|
||||||
|
|
||||||
|
All diagrams are created using ASCII art for:
|
||||||
|
- Easy version control (text-based)
|
||||||
|
- Universal compatibility (no special tools needed)
|
||||||
|
- Fast loading and rendering
|
||||||
|
- Copy-paste friendly
|
||||||
|
|
||||||
|
## Reading Diagrams
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────┐
|
||||||
|
│ Box │ = Component or module
|
||||||
|
└─────┘
|
||||||
|
|
||||||
|
↓ = Data flow direction
|
||||||
|
→
|
||||||
|
|
||||||
|
┌─┬─┐
|
||||||
|
│A│B│ = Multiple components side by side
|
||||||
|
└─┴─┘
|
||||||
|
|
||||||
|
┌───────┐
|
||||||
|
│ ┌───┤ = Nested components
|
||||||
|
│ └───┘
|
||||||
|
└───────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Solid lines** (`─`, `│`): Direct dependencies
|
||||||
|
- **Arrows** (`→`, `↓`): Data flow direction
|
||||||
|
- **Boxes** (`┌─┐`): Components, modules, files
|
||||||
|
- **Double lines** (`═`, `║`): Important/critical paths
|
||||||
|
- **Dotted** (`:`, `.`): Optional or conditional paths
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
# Middleware Pattern in Go
|
||||||
|
|
||||||
|
## Pattern Overview
|
||||||
|
|
||||||
|
The Middleware Pattern wraps HTTP handlers to add cross-cutting concerns like logging, authentication, error recovery, and request preprocessing. It follows the decorator pattern, allowing you to compose multiple middleware into a chain.
|
||||||
|
|
||||||
|
## Pattern Structure
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Middleware function signature
|
||||||
|
type Middleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
|
// Middleware wraps a handler
|
||||||
|
func MyMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Pre-processing (before handler)
|
||||||
|
// ... do something before
|
||||||
|
|
||||||
|
// Call next handler
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
// Post-processing (after handler)
|
||||||
|
// ... do something after
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real Implementation from Project
|
||||||
|
|
||||||
|
### Preferences Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/middleware/preferences.go
|
||||||
|
|
||||||
|
// PreferencesMiddleware reads user preference cookies and stores them in request context
|
||||||
|
func PreferencesMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Pre-processing: Read cookies
|
||||||
|
prefs := &Preferences{
|
||||||
|
CVLength: getCookieWithDefault(r, "cv-length", "short"),
|
||||||
|
CVIcons: getCookieWithDefault(r, "cv-icons", "show"),
|
||||||
|
CVLanguage: getCookieWithDefault(r, "cv-language", "en"),
|
||||||
|
CVTheme: getCookieWithDefault(r, "cv-theme", "default"),
|
||||||
|
ColorTheme: getCookieWithDefault(r, "color-theme", "light"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate old values
|
||||||
|
if prefs.CVLength == "extended" {
|
||||||
|
prefs.CVLength = "long"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in context
|
||||||
|
ctx := context.WithValue(r.Context(), PreferencesKey, prefs)
|
||||||
|
|
||||||
|
// Call next handler with modified context
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
|
||||||
|
// No post-processing needed for this middleware
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recovery Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/middleware/recovery.go
|
||||||
|
|
||||||
|
// Recovery catches panics and returns 500 error
|
||||||
|
func Recovery(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Setup panic recovery
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
// Log panic with stack trace
|
||||||
|
log.Printf("PANIC: %v\n%s", err, debug.Stack())
|
||||||
|
|
||||||
|
// Return error response
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call next handler (protected by defer/recover)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logger Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/middleware/logger.go
|
||||||
|
|
||||||
|
// Logger logs HTTP requests and their duration
|
||||||
|
func Logger(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Pre-processing: Start timer and log request
|
||||||
|
start := time.Now()
|
||||||
|
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||||
|
|
||||||
|
// Wrap ResponseWriter to capture status code
|
||||||
|
wrapped := &responseWriter{
|
||||||
|
ResponseWriter: w,
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call next handler
|
||||||
|
next.ServeHTTP(wrapped, r)
|
||||||
|
|
||||||
|
// Post-processing: Log duration and status
|
||||||
|
duration := time.Since(start)
|
||||||
|
log.Printf("Completed in %v (status: %d)", duration, wrapped.statusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to capture response status
|
||||||
|
type responseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *responseWriter) WriteHeader(code int) {
|
||||||
|
rw.statusCode = code
|
||||||
|
rw.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware Composition
|
||||||
|
|
||||||
|
### Chaining Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/routes/routes.go
|
||||||
|
|
||||||
|
func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
mux.HandleFunc("/", cvHandler.Home)
|
||||||
|
mux.HandleFunc("/cv", cvHandler.CVContent)
|
||||||
|
mux.HandleFunc("/health", healthHandler.Health)
|
||||||
|
|
||||||
|
// Compose middleware chain
|
||||||
|
// Execution order: Recovery → Logger → SecurityHeaders → Preferences → mux
|
||||||
|
handler := middleware.Recovery(
|
||||||
|
middleware.Logger(
|
||||||
|
middleware.SecurityHeaders(
|
||||||
|
middleware.PreferencesMiddleware(mux),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route-Specific Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Apply middleware only to specific routes
|
||||||
|
func Setup(cvHandler *handlers.CVHandler) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Public routes (minimal middleware)
|
||||||
|
mux.HandleFunc("/", cvHandler.Home)
|
||||||
|
mux.HandleFunc("/health", healthHandler.Health)
|
||||||
|
|
||||||
|
// Protected PDF route (additional middleware)
|
||||||
|
pdfHandler := middleware.OriginChecker(
|
||||||
|
middleware.RateLimiter(
|
||||||
|
http.HandlerFunc(cvHandler.ExportPDF),
|
||||||
|
3, // 3 requests per minute
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mux.Handle("/export/pdf", pdfHandler)
|
||||||
|
|
||||||
|
// Global middleware for all routes
|
||||||
|
handler := middleware.Recovery(
|
||||||
|
middleware.Logger(
|
||||||
|
middleware.PreferencesMiddleware(mux),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Middleware Use Cases
|
||||||
|
|
||||||
|
### 1. Authentication
|
||||||
|
|
||||||
|
```go
|
||||||
|
func AuthMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get token from header
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
|
||||||
|
// Validate token
|
||||||
|
userID, err := validateToken(token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user ID in context
|
||||||
|
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. CORS
|
||||||
|
|
||||||
|
```go
|
||||||
|
func CORS(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Set CORS headers
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
// Handle preflight
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Request Timeout
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Timeout(duration time.Duration) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Create context with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), duration)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Create channel for handler completion
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for completion or timeout
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Handler completed
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Timeout occurred
|
||||||
|
http.Error(w, "Request Timeout", http.StatusGatewayTimeout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Request ID
|
||||||
|
|
||||||
|
```go
|
||||||
|
func RequestID(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Generate unique ID
|
||||||
|
requestID := uuid.New().String()
|
||||||
|
|
||||||
|
// Add to response header
|
||||||
|
w.Header().Set("X-Request-ID", requestID)
|
||||||
|
|
||||||
|
// Store in context
|
||||||
|
ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware Execution Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Request
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Recovery Middleware │ ← Outermost (catches all panics)
|
||||||
|
│ defer/recover │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Logger Middleware │ ← Logs request + duration
|
||||||
|
│ Pre: Log request │
|
||||||
|
│ Post: Log duration │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Security Middleware │ ← Add security headers
|
||||||
|
│ Set headers │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Preferences Middleware │ ← Innermost (closest to handler)
|
||||||
|
│ Read cookies → context │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Handler │ ← Business logic
|
||||||
|
│ Process request │
|
||||||
|
└─────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Response (unwraps in reverse order)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Separation of Concerns**: Cross-cutting logic separate from handlers
|
||||||
|
2. **Composability**: Chain multiple middleware together
|
||||||
|
3. **Reusability**: Same middleware for multiple routes
|
||||||
|
4. **Testability**: Easy to test in isolation
|
||||||
|
5. **Maintainability**: Change behavior without touching handlers
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### ✅ DO
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Keep middleware focused on one concern
|
||||||
|
func LoggerMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Only logging logic here
|
||||||
|
log.Printf("[%s] %s", r.Method, r.URL.Path)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use context for request-scoped values
|
||||||
|
func PreferencesMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
prefs := readPreferences(r)
|
||||||
|
ctx := context.WithValue(r.Context(), PrefsKey, prefs)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order middleware correctly (outer to inner)
|
||||||
|
handler := Recovery(Logger(Auth(mux)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T
|
||||||
|
|
||||||
|
```go
|
||||||
|
// DON'T mix multiple concerns in one middleware
|
||||||
|
func BadMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Too much! Logging, auth, CORS, caching...
|
||||||
|
log.Print(r.URL)
|
||||||
|
if !checkAuth(r) { return }
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
cached := getCache(r.URL.Path)
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DON'T store context in struct
|
||||||
|
type BadMiddleware struct {
|
||||||
|
ctx context.Context // Wrong!
|
||||||
|
}
|
||||||
|
|
||||||
|
// DON'T modify original request (use r.WithContext)
|
||||||
|
func BadMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Header.Set("X-Foo", "bar") // Modifies original!
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestPreferencesMiddleware(t *testing.T) {
|
||||||
|
// Create test handler that reads preferences
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
prefs := GetPreferences(r)
|
||||||
|
if prefs.CVLength != "long" {
|
||||||
|
t.Errorf("expected long, got %s", prefs.CVLength)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wrap with middleware
|
||||||
|
wrapped := PreferencesMiddleware(handler)
|
||||||
|
|
||||||
|
// Create test request with cookie
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "cv-length", Value: "long"})
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
wrapped.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
|
||||||
|
- **Chain of Responsibility**: Middleware is a specific implementation
|
||||||
|
- **Decorator Pattern**: Wrapping handlers adds behavior
|
||||||
|
- **Context Pattern**: Often used together for request-scoped data
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [Writing Middleware in Go](https://www.alexedwards.net/blog/making-and-using-middleware)
|
||||||
|
- [Middleware Pattern in Go](https://gowebexamples.com/advanced-middleware/)
|
||||||
|
- [Context Pattern](./03-context-pattern.md) - Used with middleware
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Go Patterns Used in This Project
|
||||||
|
|
||||||
|
This directory contains documentation on the Go design patterns and idioms used throughout the CV website project.
|
||||||
|
|
||||||
|
## Pattern Catalog
|
||||||
|
|
||||||
|
1. **[Middleware Pattern](./01-middleware-pattern.md)** - HTTP middleware chain for cross-cutting concerns
|
||||||
|
2. **[Handler Pattern](./02-handler-pattern.md)** - Organized HTTP handler structure
|
||||||
|
3. **[Context Pattern](./03-context-pattern.md)** - Request-scoped values using context
|
||||||
|
4. **[Error Wrapping](./04-error-wrapping.md)** - Structured error handling with wrapping
|
||||||
|
5. **[Dependency Injection](./05-dependency-injection.md)** - Constructor-based dependency injection
|
||||||
|
6. **[Template Pattern](./06-template-pattern.md)** - Cached template management
|
||||||
|
7. **[Singleton Pattern](./07-singleton-pattern.md)** - Single instance managers (template, config)
|
||||||
|
8. **[Factory Pattern](./08-factory-pattern.md)** - Error and response constructors
|
||||||
|
|
||||||
|
## Pattern Categories
|
||||||
|
|
||||||
|
### Structural Patterns
|
||||||
|
- **Middleware Pattern** - Composable request processing
|
||||||
|
- **Singleton Pattern** - Single instance coordination
|
||||||
|
- **Dependency Injection** - Decoupled component initialization
|
||||||
|
|
||||||
|
### Behavioral Patterns
|
||||||
|
- **Handler Pattern** - Request routing and handling
|
||||||
|
- **Context Pattern** - Request-scoped data propagation
|
||||||
|
- **Template Pattern** - Flexible rendering engine
|
||||||
|
|
||||||
|
### Error Handling Patterns
|
||||||
|
- **Error Wrapping** - Context-rich error chains
|
||||||
|
- **Typed Errors** - Domain-specific error types
|
||||||
|
- **Factory Pattern** - Consistent error creation
|
||||||
|
|
||||||
|
## Pattern Usage Map
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Pattern Usage Map │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
main.go
|
||||||
|
├─→ Singleton Pattern (config, template manager)
|
||||||
|
├─→ Dependency Injection (handler construction)
|
||||||
|
└─→ Middleware Pattern (chain setup)
|
||||||
|
|
||||||
|
internal/handlers/
|
||||||
|
├─→ Handler Pattern (method organization)
|
||||||
|
├─→ Error Wrapping (error handling)
|
||||||
|
├─→ Factory Pattern (error/response creation)
|
||||||
|
└─→ Context Pattern (preference access)
|
||||||
|
|
||||||
|
internal/middleware/
|
||||||
|
├─→ Middleware Pattern (http.Handler wrapping)
|
||||||
|
├─→ Context Pattern (value storage)
|
||||||
|
└─→ Error Wrapping (panic recovery)
|
||||||
|
|
||||||
|
internal/templates/
|
||||||
|
├─→ Singleton Pattern (manager instance)
|
||||||
|
├─→ Template Pattern (rendering strategy)
|
||||||
|
└─→ Dependency Injection (config injection)
|
||||||
|
|
||||||
|
internal/models/
|
||||||
|
├─→ Factory Pattern (model loading)
|
||||||
|
└─→ Error Wrapping (validation errors)
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use Each Pattern
|
||||||
|
|
||||||
|
### Middleware Pattern
|
||||||
|
✓ Cross-cutting concerns (logging, auth, CORS)
|
||||||
|
✓ Request/response modification
|
||||||
|
✓ Chain-of-responsibility needs
|
||||||
|
✗ Business logic (use handlers instead)
|
||||||
|
|
||||||
|
### Handler Pattern
|
||||||
|
✓ HTTP request handling
|
||||||
|
✓ Route-specific logic
|
||||||
|
✓ Organizing endpoints by resource
|
||||||
|
✗ Generic utilities (use packages instead)
|
||||||
|
|
||||||
|
### Context Pattern
|
||||||
|
✓ Request-scoped values (user, preferences)
|
||||||
|
✓ Cancellation signals
|
||||||
|
✓ Deadlines and timeouts
|
||||||
|
✗ Function parameters (use explicit params)
|
||||||
|
|
||||||
|
### Error Wrapping
|
||||||
|
✓ Adding context to errors
|
||||||
|
✓ Preserving error chains
|
||||||
|
✓ Debug information
|
||||||
|
✗ Simple errors (use errors.New)
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
✓ Decoupling components
|
||||||
|
✓ Testing with mocks
|
||||||
|
✓ Configuration flexibility
|
||||||
|
✗ Simple functions (use direct calls)
|
||||||
|
|
||||||
|
### Template Pattern
|
||||||
|
✓ Flexible rendering
|
||||||
|
✓ HTML generation
|
||||||
|
✓ Hot reload in development
|
||||||
|
✗ JSON APIs (use direct encoding)
|
||||||
|
|
||||||
|
### Singleton Pattern
|
||||||
|
✓ Shared resources (DB, cache)
|
||||||
|
✓ Configuration managers
|
||||||
|
✓ Template engines
|
||||||
|
✗ Stateless utilities (use packages)
|
||||||
|
|
||||||
|
### Factory Pattern
|
||||||
|
✓ Complex object creation
|
||||||
|
✓ Consistent initialization
|
||||||
|
✓ Error construction
|
||||||
|
✗ Simple structs (use literals)
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
### ❌ Global State
|
||||||
|
```go
|
||||||
|
// BAD: Mutable global variable
|
||||||
|
var globalConfig Config
|
||||||
|
|
||||||
|
// GOOD: Pass as dependency
|
||||||
|
func NewHandler(config *Config) *Handler
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Panic for Flow Control
|
||||||
|
```go
|
||||||
|
// BAD: Using panic for expected errors
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Return errors
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("operation failed: %w", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Ignoring Errors
|
||||||
|
```go
|
||||||
|
// BAD: Ignoring error
|
||||||
|
_ = json.Unmarshal(data, &result)
|
||||||
|
|
||||||
|
// GOOD: Handle error
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal: %w", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Context in Structs
|
||||||
|
```go
|
||||||
|
// BAD: Storing context in struct
|
||||||
|
type Handler struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Pass context as first parameter
|
||||||
|
func (h *Handler) Handle(ctx context.Context, w, r)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Naked Returns
|
||||||
|
```go
|
||||||
|
// BAD: Naked return with named results
|
||||||
|
func process() (result string, err error) {
|
||||||
|
result = "foo"
|
||||||
|
return // Confusing!
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Explicit return
|
||||||
|
func process() (string, error) {
|
||||||
|
result := "foo"
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Learning Path
|
||||||
|
|
||||||
|
For developers new to these patterns:
|
||||||
|
|
||||||
|
1. **Start with**: Handler Pattern, Error Wrapping
|
||||||
|
2. **Then learn**: Middleware Pattern, Context Pattern
|
||||||
|
3. **Advanced**: Dependency Injection, Template Pattern
|
||||||
|
4. **Master**: Singleton Pattern, Factory Pattern
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Effective Go](https://golang.org/doc/effective_go) - Official Go style guide
|
||||||
|
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) - Common mistakes
|
||||||
|
- [Practical Go](https://dave.cheney.net/practical-go) - Best practices
|
||||||
|
|
||||||
|
## Pattern Evolution
|
||||||
|
|
||||||
|
This project evolved through these pattern adoptions:
|
||||||
|
|
||||||
|
### Phase 1: Basic Structure
|
||||||
|
- Simple handlers
|
||||||
|
- No middleware
|
||||||
|
- Manual cookie reading
|
||||||
|
|
||||||
|
### Phase 2: Middleware Introduction
|
||||||
|
- PreferencesMiddleware added
|
||||||
|
- Cookie handling centralized
|
||||||
|
- Context pattern adopted
|
||||||
|
|
||||||
|
### Phase 3: Type Safety
|
||||||
|
- Request/response types
|
||||||
|
- Validation tags
|
||||||
|
- Typed errors
|
||||||
|
|
||||||
|
### Phase 4: Error Handling
|
||||||
|
- Error wrapping throughout
|
||||||
|
- Domain error types
|
||||||
|
- Centralized error handler
|
||||||
|
|
||||||
|
### Phase 5: Testing
|
||||||
|
- Dependency injection for testability
|
||||||
|
- Mock-friendly interfaces
|
||||||
|
- Benchmark tests
|
||||||
Reference in New Issue
Block a user