Skip to content

Commit

Permalink
refactor(api-server): Decouple server and command
Browse files Browse the repository at this point in the history
With this commit, the server and the command are decoupled. The command
now rests in the conventional /cmd/<command-name> location. The new
server architecture opens up the possibility of better testing, too,
with the use of the `amizone.ClientFactoryInterface` for the
authentication handler layer.
  • Loading branch information
ditsuke committed May 5, 2022
1 parent ed87958 commit b49928f
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 59 deletions.
15 changes: 0 additions & 15 deletions amizone_api/handlers/handler_config.go

This file was deleted.

1 change: 0 additions & 1 deletion amizone_api/main_test.go

This file was deleted.

22 changes: 9 additions & 13 deletions amizone_api/main.go → cmd/amizone-api-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package main

import (
"context"
"github.com/ditsuke/go-amizone/amizone_api/handlers"
"github.com/ditsuke/go-amizone/server"
"github.com/joho/godotenv"
"k8s.io/klog/v2"
"net/http"
Expand All @@ -20,22 +20,18 @@ func main() {
}
address := os.Getenv("AMIZONE_API_ADDRESS")

a := handlers.NewHandlerCfg(logger)

mux := http.NewServeMux()
mux.HandleFunc("/attendance", a.AttendanceHandler)
mux.HandleFunc("/schedule", a.ClassScheduleHandler)
mux.HandleFunc("/exam_schedule", a.ExamScheduleHandler)

server := http.Server{
Addr: address,
Handler: mux,
s := server.ApiServer{
Config: &server.Config{
Logger: logger,
BindAddr: address,
},
Router: http.NewServeMux(),
}

// Start the server on a new go-thread
go func() {
logger.Info("Starting server", "address", address)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := s.Run(); err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
Expand All @@ -54,7 +50,7 @@ func main() {
ctx, cancelFunc := context.WithTimeout(context.Background(), 20*time.Second)
defer cancelFunc()

err = server.Shutdown(ctx)
err = s.Stop(ctx)
if err != nil {
logger.Error(err, "failed to gracefully shut down serer", err)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/amizone-api-server/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package main
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,11 @@ import (
"net/http"
)

// AuthenticatedHandler functions handle requests to Amizone that require auth.
// They need to be wrapped by a decorator that checks the auth parameters and creates an amizone.ClientInterface
// instance before calling onto the authenticated handler.
type AuthenticatedHandler func(
rw http.ResponseWriter,
r *http.Request,
c amizone.ClientInterface)

// authenticatedHandlerWrapper wraps an AuthenticatedHandler, composing a http.HandlerFunc
// This function handles retrieving authentication information from the request, initializing
// an amizone.amizoneClient with the information, and then passing this to the handler.
// This function also handles authentication errors if the auth information is invalid.
func authenticatedHandlerWrapper(a *handlerCfg, handler AuthenticatedHandler) http.HandlerFunc {
func authenticatedHandlerWrapper(c *Cfg, handler AuthenticatedHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get query parameters for auth
username := r.FormValue("username")
Expand All @@ -28,7 +20,7 @@ func authenticatedHandlerWrapper(a *handlerCfg, handler AuthenticatedHandler) ht
return
}

client, err := amizone.NewClient(
client, err := c.A(
amizone.Credentials{
Username: username,
Password: password,
Expand All @@ -40,7 +32,7 @@ func authenticatedHandlerWrapper(a *handlerCfg, handler AuthenticatedHandler) ht
w.WriteHeader(http.StatusUnauthorized)
return
}
a.l.Error(err, "error creating amizone client")
c.L.Error(err, "error creating amizone client")
w.WriteHeader(http.StatusInternalServerError)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,35 @@ package handlers

import (
"github.com/ditsuke/go-amizone/amizone"
"github.com/ditsuke/go-amizone/amizone_api/response_models"
"github.com/ditsuke/go-amizone/server/response_models"
"net/http"
"time"
)

func (a *handlerCfg) authenticatedAttendanceHandler(rw http.ResponseWriter, r *http.Request, c amizone.ClientInterface) {
// AuthenticatedHandler functions handle requests to Amizone that require auth.
// They need to be wrapped by a decorator that checks the auth parameters and creates an amizone.ClientInterface
// instance before calling onto the authenticated handler.
type AuthenticatedHandler func(
rw http.ResponseWriter,
r *http.Request,
c amizone.ClientInterface)

func (a *Cfg) authenticatedAttendanceHandler(rw http.ResponseWriter, r *http.Request, c amizone.ClientInterface) {
if r.Method == "GET" {
attendance, err := c.GetAttendance()
if err != nil {
a.l.Error(err, "Failed to get attendance from the amizone client", "client", c)
a.L.Error(err, "Failed to get attendance from the amizone client", "client", c)
rw.WriteHeader(http.StatusInternalServerError)
}
err = WriteJsonResponse(attendance, rw)
if err != nil {
a.l.Error(err, "Failed to write attendance to the response writer", "client", c)
a.L.Error(err, "Failed to write attendance to the response writer", "client", c)
rw.WriteHeader(http.StatusInternalServerError)
}
}
}

func (a *handlerCfg) authenticatedClassScheduleHandler(rw http.ResponseWriter, r *http.Request, c amizone.ClientInterface) {
func (a *Cfg) authenticatedClassScheduleHandler(rw http.ResponseWriter, r *http.Request, c amizone.ClientInterface) {
var err error
if r.Method == "GET" {
t := time.Now()
Expand All @@ -42,28 +50,16 @@ func (a *handlerCfg) authenticatedClassScheduleHandler(rw http.ResponseWriter, r
}
}

func (a *handlerCfg) authenticatedExamScheduleHandler(rw http.ResponseWriter, r *http.Request, c amizone.ClientInterface) {
func (a *Cfg) authenticatedExamScheduleHandler(rw http.ResponseWriter, r *http.Request, c amizone.ClientInterface) {
if r.Method == http.MethodGet {
schedule, err := c.GetExamSchedule()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
}
err = WriteJsonResponse(schedule, rw)
if err != nil {
a.l.Error(err, "Failed to write exam schedule to the response writer", "client", c)
a.L.Error(err, "Failed to write exam schedule to the response writer", "client", c)
rw.WriteHeader(http.StatusInternalServerError)
}
}
}

func (a *handlerCfg) AttendanceHandler(rw http.ResponseWriter, r *http.Request) {
authenticatedHandlerWrapper(a, a.authenticatedAttendanceHandler)(rw, r)
}

func (a *handlerCfg) ClassScheduleHandler(rw http.ResponseWriter, r *http.Request) {
authenticatedHandlerWrapper(a, a.authenticatedClassScheduleHandler)(rw, r)
}

func (a *handlerCfg) ExamScheduleHandler(rw http.ResponseWriter, r *http.Request) {
authenticatedHandlerWrapper(a, a.authenticatedExamScheduleHandler)(rw, r)
}
File renamed without changes.
30 changes: 30 additions & 0 deletions server/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package handlers

import (
"github.com/ditsuke/go-amizone/amizone"
"github.com/go-logr/logr"
"net/http"
)

// Cfg is a handler configuration struct for the API.
// It currently provides the handlers access to the logger configured by the main application.
type Cfg struct {
L logr.Logger
A amizone.ClientFactoryInterface
}

func NewCfg(l logr.Logger, a amizone.ClientFactoryInterface) *Cfg {
return &Cfg{L: l, A: a}
}

func (a *Cfg) AttendanceHandler(rw http.ResponseWriter, r *http.Request) {
authenticatedHandlerWrapper(a, a.authenticatedAttendanceHandler)(rw, r)
}

func (a *Cfg) ClassScheduleHandler(rw http.ResponseWriter, r *http.Request) {
authenticatedHandlerWrapper(a, a.authenticatedClassScheduleHandler)(rw, r)
}

func (a *Cfg) ExamScheduleHandler(rw http.ResponseWriter, r *http.Request) {
authenticatedHandlerWrapper(a, a.authenticatedExamScheduleHandler)(rw, r)
}
File renamed without changes.
File renamed without changes.
63 changes: 63 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package server

import (
"context"
"github.com/ditsuke/go-amizone/amizone"
"github.com/ditsuke/go-amizone/server/handlers"
"github.com/go-logr/logr"
"net/http"
)

type AmizoneClientFactory func(amizone.Credentials) amizone.ClientInterface

type Config struct {
Logger logr.Logger
BindAddr string
}

func NewConfig() *Config {
return &Config{
BindAddr: "127.0.0.1:8081",
}
}

type ApiServer struct {
Config *Config
Router *http.ServeMux
httpServer *http.Server
}

func New(config *Config) *ApiServer {
return &ApiServer{
Config: config,
Router: http.NewServeMux(),
}
}

func (s *ApiServer) Run() error {
if s.httpServer == nil {
s.Config.Logger.V(1).Info("Initializing server...")
s.httpServer = &http.Server{
Addr: s.Config.BindAddr,
Handler: s.Router,
}
}

s.Config.Logger.Info("Starting server", "bind_addr", s.Config.BindAddr)
s.configureRouter()
return s.httpServer.ListenAndServe()
}

func (s *ApiServer) Stop(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}

func (s *ApiServer) configureRouter() {
handlerCfg := handlers.NewCfg(s.Config.Logger, func(cred amizone.Credentials, httpClient *http.Client) (amizone.ClientInterface, error) {
return amizone.NewClient(cred, httpClient)
})

s.Router.HandleFunc("/attendance", handlerCfg.AttendanceHandler)
s.Router.HandleFunc("/class_schedule", handlerCfg.ClassScheduleHandler)
s.Router.HandleFunc("/exam_schedule", handlerCfg.ExamScheduleHandler)
}

0 comments on commit b49928f

Please sign in to comment.