Commit 0bef608

mo khan <mo@mokhan.ca>
2025-04-13 00:11:05
refactor: extract mountable interface to allow controllers to mount themselves into the mux
1 parent 5471583
app/controllers/health/controller.go
@@ -9,6 +9,10 @@ func New() *Controller {
 	return &Controller{}
 }
 
+func (c *Controller) MountTo(mux *http.ServeMux) {
+	mux.HandleFunc("GET /health", c.Index)
+}
+
 func (c *Controller) Index(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
app/controllers/sparkles/controller.go
@@ -16,6 +16,11 @@ func New(db db.Repository) *Controller {
 	return &Controller{db: db}
 }
 
+func (c *Controller) MountTo(mux *http.ServeMux) {
+	mux.HandleFunc("GET /sparkles", c.Index)
+	mux.HandleFunc("POST /sparkles", c.Create)
+}
+
 func (c *Controller) Index(w http.ResponseWriter, r *http.Request) {
 	serde.ToHTTP(w, r, c.db.All())
 }
pkg/web/sparkles_test.go → app/controllers/sparkles/controller_test.go
@@ -1,4 +1,4 @@
-package web
+package sparkles
 
 import (
 	"net/http"
@@ -19,7 +19,7 @@ func TestSparkles(t *testing.T) {
 		store := db.NewRepository()
 		store.Save(sparkle)
 
-		server := New(store)
+		controller := New(store)
 
 		t.Run("returns JSON", func(t *testing.T) {
 			request := test.Request("GET", "/sparkles",
@@ -27,7 +27,7 @@ func TestSparkles(t *testing.T) {
 			)
 			response := httptest.NewRecorder()
 
-			server.ServeHTTP(response, request)
+			controller.Index(response, request)
 
 			assert.Equal(t, http.StatusOK, response.Code)
 			items, err := serde.FromJSON[[]*domain.Sparkle](response.Body)
@@ -42,13 +42,13 @@ func TestSparkles(t *testing.T) {
 	t.Run("POST /sparkles", func(t *testing.T) {
 		t.Run("saves a new sparkle", func(t *testing.T) {
 			repository := db.NewRepository()
-			server := New(repository)
+			controller := New(repository)
 
 			sparkle, _ := domain.NewSparkle("@tanuki for reviewing my MR!")
 			request := test.Request("POST", "/sparkles", test.WithContentType(sparkle, serde.JSON))
 			response := httptest.NewRecorder()
 
-			server.ServeHTTP(response, request)
+			controller.Create(response, request)
 
 			require.Equal(t, http.StatusCreated, response.Code)
 			assert.Equal(t, 1, len(repository.All()))
app/app.go
@@ -0,0 +1,27 @@
+package app
+
+import (
+	"net/http"
+
+	"github.com/xlgmokha/x/pkg/ioc"
+	"gitlab.com/mokhax/sparkled/app/controllers/health"
+	"gitlab.com/mokhax/sparkled/app/controllers/sparkles"
+	"gitlab.com/mokhax/sparkled/pkg/db"
+	"gitlab.com/mokhax/sparkled/pkg/web"
+)
+
+func New() http.Handler {
+	mux := ioc.MustResolve[*http.ServeMux](ioc.Default)
+
+	mountable := []web.Mountable{
+		sparkles.New(ioc.MustResolve[db.Repository](ioc.Default)),
+		health.New(),
+	}
+	for _, m := range mountable {
+		m.MountTo(mux)
+	}
+
+	mux.Handle("GET /", http.FileServer(http.Dir("public")))
+
+	return mux
+}
app/app_test.go
@@ -0,0 +1,32 @@
+package app
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"gitlab.com/mokhax/sparkled/pkg/test"
+)
+
+func TestApp(t *testing.T) {
+	t.Run("New", func(t *testing.T) {
+		server := New()
+
+		t.Run("GET /index.html", func(t *testing.T) {
+			t.Skip()
+			response := httptest.NewRecorder()
+
+			server.ServeHTTP(response, test.Request("GET", "/"))
+			assert.Equal(t, http.StatusOK, response.Code)
+			assert.Contains(t, response.Body.String(), "SparkleLab")
+		})
+
+		t.Run("GET /health", func(t *testing.T) {
+			response := httptest.NewRecorder()
+
+			server.ServeHTTP(response, test.Request("GET", "/health"))
+			assert.Equal(t, http.StatusOK, response.Code)
+		})
+	})
+}
app/init.go
@@ -0,0 +1,25 @@
+package app
+
+import (
+	"net/http"
+
+	"github.com/xlgmokha/x/pkg/ioc"
+	"gitlab.com/mokhax/sparkled/app/controllers/health"
+	"gitlab.com/mokhax/sparkled/app/controllers/sparkles"
+	"gitlab.com/mokhax/sparkled/pkg/db"
+)
+
+func init() {
+	ioc.RegisterSingleton[db.Repository](ioc.Default, func() db.Repository {
+		return db.NewRepository()
+	})
+	ioc.RegisterSingleton[*http.ServeMux](ioc.Default, func() *http.ServeMux {
+		return http.NewServeMux()
+	})
+	ioc.Register[*sparkles.Controller](ioc.Default, func() *sparkles.Controller {
+		return sparkles.New(ioc.MustResolve[db.Repository](ioc.Default))
+	})
+	ioc.Register[*health.Controller](ioc.Default, func() *health.Controller {
+		return health.New()
+	})
+}
cmd/sparkled/main.go
@@ -5,26 +5,15 @@ import (
 	"net/http"
 
 	"github.com/xlgmokha/x/pkg/env"
-	"github.com/xlgmokha/x/pkg/ioc"
-	"gitlab.com/mokhax/sparkled/pkg/db"
-	"gitlab.com/mokhax/sparkled/pkg/web"
+	"gitlab.com/mokhax/sparkled/app"
 )
 
-func init() {
-	ioc.RegisterSingleton[db.Repository](ioc.Default, func() db.Repository {
-		return db.NewRepository()
-	})
-	ioc.RegisterSingleton[*web.Server](ioc.Default, func() *web.Server {
-		return web.New(ioc.MustResolve[db.Repository](ioc.Default))
-	})
-}
-
 func main() {
 	bindAddr := env.Fetch("BIND_ADDR", ":http")
 	log.Printf("Listening on %v\n", bindAddr)
 
 	log.Fatal(http.ListenAndServe(
 		bindAddr,
-		ioc.MustResolve[*web.Server](ioc.Default),
+		app.New(),
 	))
 }
pkg/web/mountable.go
@@ -0,0 +1,7 @@
+package web
+
+import "net/http"
+
+type Mountable interface {
+	MountTo(*http.ServeMux)
+}
pkg/web/server.go
@@ -1,28 +0,0 @@
-package web
-
-import (
-	"net/http"
-
-	"gitlab.com/mokhax/sparkled/app/controllers/health"
-	"gitlab.com/mokhax/sparkled/app/controllers/sparkles"
-	"gitlab.com/mokhax/sparkled/pkg/db"
-)
-
-type Server struct {
-	mux *http.ServeMux
-}
-
-func New(storage db.Repository) *Server {
-	mux := http.NewServeMux()
-	c := sparkles.New(storage)
-	mux.HandleFunc("GET /sparkles", c.Index)
-	mux.HandleFunc("POST /sparkles", c.Create)
-	mux.HandleFunc("GET /health", health.New().Index)
-	mux.Handle("GET /", http.FileServer(http.Dir("public")))
-
-	return &Server{mux: mux}
-}
-
-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	s.mux.ServeHTTP(w, r)
-}
pkg/web/server_test.go
@@ -1,31 +0,0 @@
-package web
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"gitlab.com/mokhax/sparkled/pkg/db"
-	"gitlab.com/mokhax/sparkled/pkg/test"
-)
-
-func TestServer(t *testing.T) {
-	server := New(db.NewRepository())
-
-	t.Run("GET /index.html", func(t *testing.T) {
-		t.Skip()
-		response := httptest.NewRecorder()
-
-		server.ServeHTTP(response, test.Request("GET", "/"))
-		assert.Equal(t, http.StatusOK, response.Code)
-		assert.Contains(t, response.Body.String(), "SparkleLab")
-	})
-
-	t.Run("GET /health", func(t *testing.T) {
-		response := httptest.NewRecorder()
-
-		server.ServeHTTP(response, test.Request("GET", "/health"))
-		assert.Equal(t, http.StatusOK, response.Code)
-	})
-}