From bc0f0efcaa5a6b4a8ca1d7e4b66acd11e17ed436 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Wed, 3 Dec 2025 22:58:21 +0100 Subject: [PATCH] feat!: migrate all examples to method dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: All examples now use automatic method dispatch instead of the Store interface with Change() method. Migrated examples: - login: Change() → Login(), Logout(), ServerWelcome() - trace-correlation: Change() → Increment(), Decrement(), Reset() - production/single-host: Change() → Increment(), Decrement(), Reset() - observability: Change() → Increment(), Decrement(), Reset() - avatar-upload: Change() → UpdateProfile(), UploadAvatarComplete() - testing/01_basic: Removed empty Change() (static page) - graceful-shutdown: Change() → Increment(), Decrement(), Reset() - chat: Change() → Send(), Join(), Leave() Note: avatar-upload action changed from "upload:avatar:complete" to "upload_avatar_complete" to match new method dispatch format. Requires: livetemplate/livetemplate#67 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- avatar-upload/main.go | 34 ++++------- chat/main.go | 108 +++++++++++++++++---------------- graceful-shutdown/main.go | 28 +++++---- login/main.go | 24 ++------ observability/main.go | 28 +++++---- production/single-host/main.go | 26 +++++--- testing/01_basic/main.go | 6 +- trace-correlation/main.go | 39 ++++++------ 8 files changed, 146 insertions(+), 147 deletions(-) diff --git a/avatar-upload/main.go b/avatar-upload/main.go index f0a5132..0d8086e 100644 --- a/avatar-upload/main.go +++ b/avatar-upload/main.go @@ -22,22 +22,7 @@ type ProfileStore struct { AvatarURL string } -// Change implements the Store interface -func (s *ProfileStore) Change(ctx *livetemplate.ActionContext) error { - log.Printf("DEBUG: Change called with action: %s", ctx.Action) - switch ctx.Action { - case "UpdateProfile": - return s.UpdateProfile(ctx) - case "upload:avatar:complete": - // Auto-triggered when avatar upload completes - log.Printf("DEBUG: Processing auto-triggered upload") - return s.ProcessAvatarUpload(ctx) - default: - return fmt.Errorf("unknown action: %s", ctx.Action) - } -} - -// UpdateProfile handles profile update form submission +// UpdateProfile handles the "UpdateProfile" action for profile update form submission func (s *ProfileStore) UpdateProfile(ctx *livetemplate.ActionContext) error { name, _ := ctx.Data.GetStringOk("name") email, _ := ctx.Data.GetStringOk("email") @@ -47,7 +32,7 @@ func (s *ProfileStore) UpdateProfile(ctx *livetemplate.ActionContext) error { // Also process avatar if it was uploaded with the form if ctx.HasUploads("avatar") { - if err := s.ProcessAvatarUpload(ctx); err != nil { + if err := s.processAvatarUpload(ctx); err != nil { return err } } @@ -56,10 +41,17 @@ func (s *ProfileStore) UpdateProfile(ctx *livetemplate.ActionContext) error { return nil } -// ProcessAvatarUpload handles avatar upload processing -// Called either automatically when upload completes (upload:avatar:complete action) +// UploadAvatarComplete handles the "upload_avatar_complete" action. +// Auto-triggered when avatar upload completes. +func (s *ProfileStore) UploadAvatarComplete(ctx *livetemplate.ActionContext) error { + log.Printf("DEBUG: Processing auto-triggered upload") + return s.processAvatarUpload(ctx) +} + +// processAvatarUpload handles avatar upload processing +// Called either automatically when upload completes (upload_avatar_complete action) // or during explicit form submission (UpdateProfile action) -func (s *ProfileStore) ProcessAvatarUpload(ctx *livetemplate.ActionContext) error { +func (s *ProfileStore) processAvatarUpload(ctx *livetemplate.ActionContext) error { // Get completed uploads from ActionContext uploads := ctx.GetCompletedUploads("avatar") log.Printf("DEBUG: ProcessAvatarUpload called, found %d completed uploads", len(uploads)) @@ -167,7 +159,7 @@ func main() { log.Printf("📸 Upload an avatar to see the upload feature in action!") log.Printf("📁 Uploaded files will be saved to ./uploads/") log.Printf("✨ Upload processing happens automatically when upload completes") - log.Printf(" (via upload:avatar:complete action) or during form submission") + log.Printf(" (via upload_avatar_complete action) or during form submission") if err := http.ListenAndServe(addr, nil); err != nil { log.Fatal(err) diff --git a/chat/main.go b/chat/main.go index 9f73f26..6753895 100644 --- a/chat/main.go +++ b/chat/main.go @@ -32,75 +32,81 @@ type User struct { IsOnline bool } -func (s *ChatState) Change(ctx *livetemplate.ActionContext) error { +// Send handles the "send" action to send a chat message +func (s *ChatState) Send(ctx *livetemplate.ActionContext) error { s.mu.Lock() defer s.mu.Unlock() - switch ctx.Action { - case "send": - var data struct { - Message string `json:"message"` - } + var data struct { + Message string `json:"message"` + } - if err := ctx.Bind(&data); err != nil { - log.Printf("Failed to bind message data: %v", err) - return nil - } + if err := ctx.Bind(&data); err != nil { + log.Printf("Failed to bind message data: %v", err) + return nil + } - if data.Message == "" { - return nil - } + if data.Message == "" { + return nil + } - s.TotalMessages++ - msg := Message{ - ID: s.TotalMessages, - Username: s.CurrentUser, - Text: data.Message, - Timestamp: time.Now().Format("15:04:05"), - } + s.TotalMessages++ + msg := Message{ + ID: s.TotalMessages, + Username: s.CurrentUser, + Text: data.Message, + Timestamp: time.Now().Format("15:04:05"), + } - s.Messages = append(s.Messages, msg) + s.Messages = append(s.Messages, msg) - // Auto-broadcast handles syncing to other tabs automatically - return nil + // Auto-broadcast handles syncing to other tabs automatically + return nil +} - case "join": - var data struct { - Username string `json:"username"` - } +// Join handles the "join" action when a user joins the chat +func (s *ChatState) Join(ctx *livetemplate.ActionContext) error { + s.mu.Lock() + defer s.mu.Unlock() - if err := ctx.Bind(&data); err != nil { - log.Printf("Failed to bind join data: %v", err) - return nil - } + var data struct { + Username string `json:"username"` + } - if data.Username == "" { - return nil - } + if err := ctx.Bind(&data); err != nil { + log.Printf("Failed to bind join data: %v", err) + return nil + } - s.CurrentUser = data.Username + if data.Username == "" { + return nil + } - if _, exists := s.Users[data.Username]; !exists { - s.Users[data.Username] = &User{ - Username: data.Username, - JoinedAt: time.Now(), - IsOnline: true, - } - s.updateOnlineCount() + s.CurrentUser = data.Username + + if _, exists := s.Users[data.Username]; !exists { + s.Users[data.Username] = &User{ + Username: data.Username, + JoinedAt: time.Now(), + IsOnline: true, } + s.updateOnlineCount() + } - return nil + return nil +} + +// Leave handles the "leave" action when a user leaves the chat +func (s *ChatState) Leave(_ *livetemplate.ActionContext) error { + s.mu.Lock() + defer s.mu.Unlock() - case "leave": - if s.CurrentUser != "" { - if user, exists := s.Users[s.CurrentUser]; exists { - user.IsOnline = false - } - s.updateOnlineCount() + if s.CurrentUser != "" { + if user, exists := s.Users[s.CurrentUser]; exists { + user.IsOnline = false } - return nil + s.updateOnlineCount() } - return nil } diff --git a/graceful-shutdown/main.go b/graceful-shutdown/main.go index 8ca92ad..4f7bc84 100644 --- a/graceful-shutdown/main.go +++ b/graceful-shutdown/main.go @@ -19,19 +19,23 @@ type CounterState struct { LastUpdated string `json:"last_updated"` } -func (s *CounterState) Change(ctx *livetemplate.ActionContext) error { - switch ctx.Action { - case "increment": - s.Counter++ - case "decrement": - s.Counter-- - case "reset": - s.Counter = 0 - default: - log.Printf("Unknown action: %s", ctx.Action) - return nil - } +// Increment handles the "increment" action +func (s *CounterState) Increment(_ *livetemplate.ActionContext) error { + s.Counter++ + s.LastUpdated = formatTime() + return nil +} + +// Decrement handles the "decrement" action +func (s *CounterState) Decrement(_ *livetemplate.ActionContext) error { + s.Counter-- + s.LastUpdated = formatTime() + return nil +} +// Reset handles the "reset" action +func (s *CounterState) Reset(_ *livetemplate.ActionContext) error { + s.Counter = 0 s.LastUpdated = formatTime() return nil } diff --git a/login/main.go b/login/main.go index 30caa5c..953f74f 100644 --- a/login/main.go +++ b/login/main.go @@ -27,21 +27,8 @@ type AuthState struct { mu sync.Mutex } -// Change handles authentication actions -func (s *AuthState) Change(ctx *livetemplate.ActionContext) error { - switch ctx.Action { - case "login": - return s.handleLogin(ctx) - case "logout": - return s.handleLogout(ctx) - case "serverWelcome": - return s.handleServerWelcome(ctx) - default: - return fmt.Errorf("unknown action: %s", ctx.Action) - } -} - -func (s *AuthState) handleLogin(ctx *livetemplate.ActionContext) error { +// Login handles the "login" action +func (s *AuthState) Login(ctx *livetemplate.ActionContext) error { username := ctx.GetString("username") password := ctx.GetString("password") @@ -82,7 +69,8 @@ func (s *AuthState) handleLogin(ctx *livetemplate.ActionContext) error { return ctx.Redirect("/", http.StatusSeeOther) } -func (s *AuthState) handleLogout(ctx *livetemplate.ActionContext) error { +// Logout handles the "logout" action +func (s *AuthState) Logout(ctx *livetemplate.ActionContext) error { s.mu.Lock() s.Username = "" s.IsLoggedIn = false @@ -100,9 +88,9 @@ func (s *AuthState) handleLogout(ctx *livetemplate.ActionContext) error { return ctx.Redirect("/", http.StatusSeeOther) } -// handleServerWelcome handles server-initiated welcome messages. +// ServerWelcome handles the "serverWelcome" action (server-initiated welcome messages). // This is triggered by TriggerAction from sendWelcomeMessage. -func (s *AuthState) handleServerWelcome(ctx *livetemplate.ActionContext) error { +func (s *AuthState) ServerWelcome(ctx *livetemplate.ActionContext) error { message := ctx.GetString("message") s.mu.Lock() s.ServerMessage = message diff --git a/observability/main.go b/observability/main.go index b33915a..371bfcd 100644 --- a/observability/main.go +++ b/observability/main.go @@ -17,19 +17,23 @@ type CounterState struct { LastUpdated string `json:"last_updated"` } -func (s *CounterState) Change(ctx *livetemplate.ActionContext) error { - switch ctx.Action { - case "increment": - s.Counter++ - case "decrement": - s.Counter-- - case "reset": - s.Counter = 0 - default: - log.Printf("Unknown action: %s", ctx.Action) - return nil - } +// Increment handles the "increment" action +func (s *CounterState) Increment(_ *livetemplate.ActionContext) error { + s.Counter++ + s.LastUpdated = formatTime() + return nil +} + +// Decrement handles the "decrement" action +func (s *CounterState) Decrement(_ *livetemplate.ActionContext) error { + s.Counter-- + s.LastUpdated = formatTime() + return nil +} +// Reset handles the "reset" action +func (s *CounterState) Reset(_ *livetemplate.ActionContext) error { + s.Counter = 0 s.LastUpdated = formatTime() return nil } diff --git a/production/single-host/main.go b/production/single-host/main.go index 4767f53..e296b29 100644 --- a/production/single-host/main.go +++ b/production/single-host/main.go @@ -22,15 +22,23 @@ type AppState struct { LastUpdated string `json:"last_updated"` } -func (s *AppState) Change(ctx *livetemplate.ActionContext) error { - switch ctx.Action { - case "increment": - s.Counter++ - case "decrement": - s.Counter-- - case "reset": - s.Counter = 0 - } +// Increment handles the "increment" action +func (s *AppState) Increment(_ *livetemplate.ActionContext) error { + s.Counter++ + s.LastUpdated = formatTime() + return nil +} + +// Decrement handles the "decrement" action +func (s *AppState) Decrement(_ *livetemplate.ActionContext) error { + s.Counter-- + s.LastUpdated = formatTime() + return nil +} + +// Reset handles the "reset" action +func (s *AppState) Reset(_ *livetemplate.ActionContext) error { + s.Counter = 0 s.LastUpdated = formatTime() return nil } diff --git a/testing/01_basic/main.go b/testing/01_basic/main.go index 7f3280c..de90058 100644 --- a/testing/01_basic/main.go +++ b/testing/01_basic/main.go @@ -15,11 +15,7 @@ type PageState struct { Count int } -// Change implements livetemplate.Store interface -func (s *PageState) Change(ctx *livetemplate.ActionContext) error { - // No actions for this static page - return nil -} +// No action methods needed for this static page func main() { // Create template (will auto-discover welcome.tmpl) diff --git a/trace-correlation/main.go b/trace-correlation/main.go index ea0099c..f6e7094 100644 --- a/trace-correlation/main.go +++ b/trace-correlation/main.go @@ -19,26 +19,27 @@ type CounterState struct { LastUpdated string `json:"last_updated"` } -func (s *CounterState) Change(ctx *livetemplate.ActionContext) error { - // Note: Trace IDs are logged automatically at the HTTP handler level - // See the TraceMiddleware wrapper below for automatic correlation - - switch ctx.Action { - case "increment": - s.Counter++ - log.Printf("Counter incremented to %d", s.Counter) - case "decrement": - s.Counter-- - log.Printf("Counter decremented to %d", s.Counter) - case "reset": - oldValue := s.Counter - s.Counter = 0 - log.Printf("Counter reset from %d to 0", oldValue) - default: - log.Printf("Unknown action: %s", ctx.Action) - return nil - } +// Increment handles the "increment" action +func (s *CounterState) Increment(_ *livetemplate.ActionContext) error { + s.Counter++ + log.Printf("Counter incremented to %d", s.Counter) + s.LastUpdated = formatTime() + return nil +} + +// Decrement handles the "decrement" action +func (s *CounterState) Decrement(_ *livetemplate.ActionContext) error { + s.Counter-- + log.Printf("Counter decremented to %d", s.Counter) + s.LastUpdated = formatTime() + return nil +} +// Reset handles the "reset" action +func (s *CounterState) Reset(_ *livetemplate.ActionContext) error { + oldValue := s.Counter + s.Counter = 0 + log.Printf("Counter reset from %d to 0", oldValue) s.LastUpdated = formatTime() return nil }