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