diff --git a/_go-learning/diagrams/01-system-architecture.md b/_go-learning/diagrams/01-system-architecture.md new file mode 100644 index 0000000..ea26efb --- /dev/null +++ b/_go-learning/diagrams/01-system-architecture.md @@ -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 diff --git a/_go-learning/diagrams/02-request-flow.md b/_go-learning/diagrams/02-request-flow.md new file mode 100644 index 0000000..b8f17fa --- /dev/null +++ b/_go-learning/diagrams/02-request-flow.md @@ -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: │ +│ └─ │ +│ │ +│ ... │ +│ │ +│ │ +│ │ +│ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 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=... │ │ +│ │ │ │ +│ │
│ │ +│ │ │ │ +│ │
│ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +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 diff --git a/_go-learning/diagrams/03-middleware-chain.md b/_go-learning/diagrams/03-middleware-chain.md new file mode 100644 index 0000000..c6ce2ff --- /dev/null +++ b/_go-learning/diagrams/03-middleware-chain.md @@ -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 diff --git a/_go-learning/diagrams/04-handler-organization.md b/_go-learning/diagrams/04-handler-organization.md new file mode 100644 index 0000000..1209b52 --- /dev/null +++ b/_go-learning/diagrams/04-handler-organization.md @@ -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 diff --git a/_go-learning/diagrams/05-data-models.md b/_go-learning/diagrams/05-data-models.md new file mode 100644 index 0000000..82fef4f --- /dev/null +++ b/_go-learning/diagrams/05-data-models.md @@ -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) + │ + └─ 3 years 2 months +``` + +## 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 diff --git a/_go-learning/diagrams/06-error-handling-flow.md b/_go-learning/diagrams/06-error-handling-flow.md new file mode 100644 index 0000000..3ae3f38 --- /dev/null +++ b/_go-learning/diagrams/06-error-handling-flow.md @@ -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): + + +Error + +

Oops! Something went wrong

+

We're sorry, but we couldn't process your request.

+

Error: INVALID_LANGUAGE

+ Go back home + + +``` + +## Error Logging + +``` +┌────────────────────────────────────────────────────────────┐ +│ Error Logging │ +└────────────────────────────────────────────────────────────┘ + +Log Format: +[ERROR] : +[ERROR] Additional context:
+[ERROR] Stack trace (if panic): + + +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 diff --git a/_go-learning/diagrams/07-template-rendering.md b/_go-learning/diagrams/07-template-rendering.md new file mode 100644 index 0000000..e475b23 --- /dev/null +++ b/_go-learning/diagrams/07-template-rendering.md @@ -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"}} + +{{end}} + +{{.Description | safe}} +// Render HTML without escaping +``` + +## Template Conditionals + +``` +┌────────────────────────────────────────────────────────────┐ +│ Template Conditionals │ +└────────────────────────────────────────────────────────────┘ + +Show/Hide based on CV length: +{{if eq .Preferences.CVLength "long"}} + +
+ {{range .Highlights}} +
  • {{.}}
  • + {{end}} +
    +{{end}} + +Show/Hide based on icons preference: +{{if eq .Preferences.CVIcons "show"}} + +{{end}} + +Conditional classes: +
    + ... +
    + +Language-specific content: +{{if eq .Preferences.CVLanguage "es"}} + Experiencia Profesional +{{else}} + Professional Experience +{{end}} + +Current vs. past experience: +{{if .Current}} + Present +{{else}} + {{formatDate .EndDate}} +{{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 diff --git a/_go-learning/diagrams/08-pdf-generation.md b/_go-learning/diagrams/08-pdf-generation.md new file mode 100644 index 0000000..4b9c47f --- /dev/null +++ b/_go-learning/diagrams/08-pdf-generation.md @@ -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}} + + +{{else}} + + +{{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 diff --git a/_go-learning/diagrams/README.md b/_go-learning/diagrams/README.md new file mode 100644 index 0000000..37cc026 --- /dev/null +++ b/_go-learning/diagrams/README.md @@ -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 diff --git a/_go-learning/patterns/01-middleware-pattern.md b/_go-learning/patterns/01-middleware-pattern.md new file mode 100644 index 0000000..d928826 --- /dev/null +++ b/_go-learning/patterns/01-middleware-pattern.md @@ -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 diff --git a/_go-learning/patterns/README.md b/_go-learning/patterns/README.md new file mode 100644 index 0000000..8efe56e --- /dev/null +++ b/_go-learning/patterns/README.md @@ -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