diff --git a/servlet/Router.go b/servlet/Router.go new file mode 100644 index 0000000..b51c58b --- /dev/null +++ b/servlet/Router.go @@ -0,0 +1,39 @@ +package servlet + +import ( + "errors" + "geniuscartel.xyz/vinegar/vinegarUtil" + "net/http" + "regexp" +) + +type ( + VinegarWebRouter struct { + Routes []*VinegarWebRoute + } + + VinegarWebRoute struct { + Pattern *regexp.Regexp + Handler VinegarHandlerFunction + Cache vinegarUtil.Cache + } +) + +func (s *VinegarWebRouter) AddRoute(route *VinegarWebRoute) { + route.Announce() + s.Routes = append(s.Routes, route) +} + +func (r *VinegarWebRouter) RouteRequest(w http.ResponseWriter, req *http.Request) error { + path := req.URL.Path + for _, route := range r.Routes { + if route.Pattern.MatchString(path) { + //fmt.Printf("SERVING: [%s]=>{%s}\n", path, route.Pattern.String()) + go route.Handler(w, req) + return nil + + } + } + + return errors.New("failed to match route for [" + path + "]") +} diff --git a/servlet/config.go b/servlet/config.go index 8474212..82dc5f5 100644 --- a/servlet/config.go +++ b/servlet/config.go @@ -96,7 +96,7 @@ func CreateBlankConfig() *Config { return &conf } -func LoadConfig(pathlike string) (*VinegarHttpServlet, error) { +func LoadConfig(pathlike string) (*VinegarWebServlet, error) { contents, err := vinegarUtil.GetDiskContent(pathlike) if err != nil { CreateBlankConfig() @@ -123,7 +123,7 @@ func LoadConfig(pathlike string) (*VinegarHttpServlet, error) { } -func (e ConfigEntry) toRoute(serv *VinegarHttpServlet) error { +func (e ConfigEntry) toRoute(serv *VinegarWebServlet) error { constructor, err := getConstructorFunction(e.ConfigType) if err != nil { return err diff --git a/servlet/dynamicRoute.go b/servlet/dynamicRoute.go index 2367127..e5e29cf 100644 --- a/servlet/dynamicRoute.go +++ b/servlet/dynamicRoute.go @@ -9,39 +9,30 @@ import ( type ( ApiRoute struct { VinegarRoute *VinegarWebRoute - HttpMethodRoutes *map[HttpMethod]VinegarHandlerFunction + HttpMethodRoutes *map[string]VinegarHandlerFunction } ) -type ( - HttpMethod int -) - -const ( - GET HttpMethod = iota - POST - PUT - PATCH - DELETE - UNDEFINED -) - -func NewApiRoute(serv *VinegarHttpServlet, pattern string) *ApiRoute { - functionMap := make(map[HttpMethod]VinegarHandlerFunction) +//NewApiRoute this will cause a panic if serv is nil +func NewApiRoute(serv *VinegarWebServlet, pattern string) *ApiRoute { + functionMap := make(map[string]VinegarHandlerFunction) ancestorRoute := NewServletRoute(pattern, createMethodHandler(&functionMap)) route := ApiRoute{ ancestorRoute, &functionMap, } - serv.AddRoute(route.VinegarRoute) + if serv != nil { //this will happen during testing + serv.Router.AddRoute(route.VinegarRoute) + } + return &route } -func createMethodHandler(m *map[HttpMethod]VinegarHandlerFunction) VinegarHandlerFunction { +func createMethodHandler(m *map[string]VinegarHandlerFunction) VinegarHandlerFunction { return func(w http.ResponseWriter, req *http.Request) { - method := getHttpMethod(req) - fn, exists := (*m)[method] + + fn, exists := (*m)[req.Method] if exists { fn(w, req) } else { @@ -55,27 +46,12 @@ func createMethodHandler(m *map[HttpMethod]VinegarHandlerFunction) VinegarHandle } } -func (api *ApiRoute) RegisterHttpMethodHandler(method HttpMethod, handler VinegarHandlerFunction) { +func (api *ApiRoute) RegisterHttpMethodHandler(method string, handler VinegarHandlerFunction) { (*api.HttpMethodRoutes)[method] = handler } -func getHttpMethod(req *http.Request) HttpMethod { - switch req.Method { - case http.MethodGet: - return GET - case "POST": - return POST - case "PUT": - return PUT - case "PATCH": - return PATCH - case "DELETE": - return DELETE - case "UNDEFINED": - default: - return UNDEFINED - } - return UNDEFINED +func (api *ApiRoute) AddGetHandler(handler VinegarHandlerFunction) { + (*api.HttpMethodRoutes)[http.MethodGet] = handler } func SendApiError(w http.ResponseWriter, httpCode int, messageCode int, message string) { diff --git a/servlet/server.go b/servlet/server.go index 868868a..e2f9135 100644 --- a/servlet/server.go +++ b/servlet/server.go @@ -2,13 +2,14 @@ package servlet import ( "encoding/json" - "errors" "fmt" "geniuscartel.xyz/vinegar/vinegarUtil" "log" + "net" "net/http" "regexp" "strconv" + "time" ) const ( @@ -24,31 +25,20 @@ type ErrorResponse struct { } type ( - // VinegarHttpServlet is the main server struct that handles HTTP requests and routing. - // It contains the TCP port to listen on, the routes to match requests against, - // and a map of status code to error handling routes. - VinegarHttpServlet struct { + VinegarWebServlet struct { Port string - Routes []*VinegarWebRoute + Router VinegarWebRouter ErrorRoutes map[int]*TemplateRoute - } - - // VinegarWebRoute defines a single route in the router. - // It contains a regex Pattern to match against the URL path, - // a Handler function to call when the route matches, - // and an optional Cache to enable caching for the route. - VinegarWebRoute struct { - Pattern *regexp.Regexp - Handler VinegarHandlerFunction - Cache vinegarUtil.Cache + interrupts chan struct{} + errors chan error } VinegarHandlerFunction func(w http.ResponseWriter, req *http.Request) ) -func NewServlet(port string) *VinegarHttpServlet { +func NewServlet(port string) *VinegarWebServlet { errs := make(map[int]*TemplateRoute) - srv := VinegarHttpServlet{Port: port, ErrorRoutes: errs} + srv := VinegarWebServlet{Port: port, ErrorRoutes: errs, interrupts: make(chan struct{})} return &srv } @@ -60,48 +50,62 @@ func NewServletRoute(routePattern string, handleFunc VinegarHandlerFunction) *Vi return &route } -func (s *VinegarHttpServlet) AddRoute(route *VinegarWebRoute) { - route.Announce() - s.Routes = append(s.Routes, route) -} - -func (s *VinegarHttpServlet) AddErrorRoute(code int, route *TemplateRoute) { +func (s *VinegarWebServlet) AddErrorRoute(code int, route *TemplateRoute) { route.Announce() s.ErrorRoutes[code] = route } -func (s *VinegarHttpServlet) ServeHTTP(w http.ResponseWriter, req *http.Request) { - path := req.URL.Path - for _, route := range s.Routes { - if route.Pattern.MatchString(path) { - //fmt.Printf("SERVING: [%s]=>{%s}\n", path, route.Pattern.String()) - go route.Handler(w, req) - return - } +func (s *VinegarWebServlet) ServeHTTP(w http.ResponseWriter, req *http.Request) { + err := s.Router.RouteRequest(w, req) + if err != nil { + s.SendError(w, req, 404, "Resource not found", err) } - s.SendError(w, req, 404, "Couldn't find your content.", errors.New("failed to match route for ["+path+"]")) } -func (s *VinegarHttpServlet) Start() error { - if len(s.Routes) < 1 { +func (s *VinegarWebServlet) Start() error { + if len(s.Router.Routes) < 1 { log.Fatal("No routes found for server. Nothing to listen and serve.") } + l, listenErr := net.Listen("tcp", s.Port) + + if listenErr != nil { + return listenErr + } + go func() { + err := http.Serve(l, s) + if err != nil { + s.errors <- err + } + }() log.Printf("Listening on [%s]\n", s.Port) + for { - err := http.ListenAndServe(s.Port, s) + select { + case err := <-s.errors: + log.Printf("server on port %s failed: %v", s.Port, err) - if err != nil { - return err + case <-s.interrupts: + err := l.Close() + if err != nil { + return err + } + return nil + } + time.Sleep(5) } return nil } +func (s *VinegarWebServlet) Shutdown() { + s.interrupts <- struct{}{} +} + func (r *VinegarWebRoute) Announce() { log.Printf("Added route for [%s]\n", r.Pattern.String()) } -func (s *VinegarHttpServlet) SendError(w http.ResponseWriter, req *http.Request, code int, msg string, aErr error) { +func (s *VinegarWebServlet) SendError(w http.ResponseWriter, req *http.Request, code int, msg string, aErr error) { fmt.Printf("[%d][%s]. Rendering template for code %d with message: %s\n", code, req.URL.Path, code, msg) out, _ := json.Marshal(aErr) fmt.Println(string(out)) diff --git a/servlet/server_test.go b/servlet/server_test.go index 3af5838..8f7d221 100644 --- a/servlet/server_test.go +++ b/servlet/server_test.go @@ -1,11 +1,16 @@ package servlet import ( + "io" + "log" + "net/http" + "net/http/httptest" + "regexp" "testing" + "time" ) func TestNewServlet(t *testing.T) { - port := ":8080" srv := NewServlet(port) @@ -14,7 +19,7 @@ func TestNewServlet(t *testing.T) { t.Errorf("Expected port %s, got %s", port, srv.Port) } - if srv.Routes != nil { + if srv.Router.Routes != nil { t.Error("Expected Routes to be nil") } @@ -22,3 +27,113 @@ func TestNewServlet(t *testing.T) { t.Error("Expected ErrorRoutes to be initialized") } } + +func TestServletServeTraffic(t *testing.T) { + + // Create a new servlet + srv := NewServlet(":8080") + + // Add a test route + srv.Router.AddRoute(&VinegarWebRoute{ + Pattern: regexp.MustCompile("/test"), + Handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello")) + }, + }) + + // Start the server in a goroutine + go srv.Start() + defer srv.Shutdown() + time.Sleep(10 * time.Second) + // Make a request to the test route + res, err := http.Get("http://localhost:8080/test") + if err != nil { + t.Fatal(err) + } + + // Verify successful response + if res.StatusCode != http.StatusOK { + t.Errorf("Expected status OK, got %d", res.StatusCode) + } + + body, _ := io.ReadAll(res.Body) + res.Body.Close() + if string(body) != "hello" { + t.Errorf("Unexpected response body %s", string(body)) + } +} + +func TestApiRoute(t *testing.T) { + + // Create a new API route + route := NewApiRoute(nil, "/hello") + + // Add a handler for GET requests + route.AddGetHandler(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("hello")) + }) + + // Create a request + req, _ := http.NewRequest(http.MethodGet, "localhost:8080/hello", nil) + + // Create a ResponseRecorder to record the response + rr := httptest.NewRecorder() + + // Call the handler + route.VinegarRoute.Handler(rr, req) + + // Check the status code + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + // Check the response body + expected := "hello" + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } +} + +func TestApiRouteStress(t *testing.T) { + route := NewApiRoute(nil, "/hello") + + route.AddGetHandler(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("hello")) + }) + + n := 1000 // number of requests + c := make(chan struct{}, n) // concurrency control + + start := time.Now() + for i := 0; i < n; i++ { + go func() { + + // Create a request + req, _ := http.NewRequest(http.MethodGet, "localhost:8080/hello", nil) + + // Create a ResponseRecorder to record the response + rr := httptest.NewRecorder() + + // Call the handler + route.VinegarRoute.Handler(rr, req) + c <- struct{}{} + }() + } + + // wait for requests to complete + for i := 0; i < n; i++ { + <-c + } + + elapsed := time.Since(start) + + // Print performance stats + log.Printf("Completed %d requests in %s", n, elapsed) + avg := elapsed / time.Duration(n) + log.Printf("Average request time: %s", avg) + + requestsPerSec := float64(n) / elapsed.Seconds() + log.Printf("Requests per second: %f", requestsPerSec) +} diff --git a/servlet/staticRoute.go b/servlet/staticRoute.go index 7f4cdc2..cc9de99 100644 --- a/servlet/staticRoute.go +++ b/servlet/staticRoute.go @@ -18,8 +18,8 @@ type ( // VinegarRoute is the base route containing the URL pattern and handler. VinegarRoute *VinegarWebRoute - // srv is the VinegarHttpServlet instance that this route is attached to. - srv *VinegarHttpServlet + // srv is the VinegarWebServlet instance that this route is attached to. + srv *VinegarWebServlet // fileRoot is the base file path to serve files from. fileRoot string @@ -32,7 +32,7 @@ type ( // //Params: // - //servlet - The VinegarHttpServlet instance to add the route to + //servlet - The VinegarWebServlet instance to add the route to // //urlPattern - The URL regex pattern for route to match // @@ -40,13 +40,13 @@ type ( // //useCache - Whether to use caching for this route // - // A RouteConstructor is a function that accepts a VinegarHttpServlet, urlPattern, file path, and cache option. It uses + // A RouteConstructor is a function that accepts a VinegarWebServlet, urlPattern, file path, and cache option. It uses // these to construct and return a FileRoute. // The return value is a FileRoute that will serve the files from the given path. // // This function signature allows encapsulating the creation of different types of FileRoutes. It is used to define // constructor functions for each file type, like NewTextRoute or NewImageRoute. - RouteConstructor func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) + RouteConstructor func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) ) // NewTextRoute creates a new FileRoute for serving text files. @@ -55,7 +55,7 @@ type ( // // Parameters: // -// servlet - The VinegarHttpServlet instance to attach the route to. +// servlet - The VinegarWebServlet instance to attach the route to. // // urlPattern - The URL regex pattern that triggers this route. // @@ -66,8 +66,8 @@ type ( // Returns: // // A FileRoute instance configured for serving text files, added to -// the provided VinegarHttpServlet. -var NewTextRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { +// the provided VinegarWebServlet. +var NewTextRoute RouteConstructor = func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { fileRoot := filepath.Clean(pathlike) if strings.Contains(fileRoot, "../") { return nil, errors.New("Traversing the directory is not allowed, use an absolute filepath instead") @@ -78,12 +78,12 @@ var NewTextRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern rootRoute := NewServletRoute(urlPattern, textRouteHandler) //i *still* kinda don't like this pattern route.VinegarRoute = rootRoute - servlet.AddRoute(route.VinegarRoute) + servlet.Router.AddRoute(route.VinegarRoute) return &route, nil } -var NewImageRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { +var NewImageRoute RouteConstructor = func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { fileRoot := filepath.Clean(pathlike) if strings.Contains(fileRoot, "../") { return nil, errors.New("Traversing the directory is not allowed, use an absolute filepath instead") @@ -93,11 +93,11 @@ var NewImageRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPatter rootRoute := NewServletRoute(urlPattern, createUncompressedFileServletFunction(&route, defaultPrune, pathlike)) route.VinegarRoute = rootRoute //i *kinda* don't like this pattern - servlet.AddRoute(route.VinegarRoute) + servlet.Router.AddRoute(route.VinegarRoute) return &route, nil } -var NewSingleFileRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { +var NewSingleFileRoute RouteConstructor = func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { route := FileRoute{ srv: servlet, fileRoot: pathlike, @@ -115,7 +115,7 @@ var NewSingleFileRoute RouteConstructor = func(servlet *VinegarHttpServlet, urlP route.VinegarRoute = parentRoute - servlet.AddRoute(route.VinegarRoute) + servlet.Router.AddRoute(route.VinegarRoute) return &route, nil } diff --git a/servlet/templateRoute.go b/servlet/templateRoute.go index 0818c1c..3af19ad 100644 --- a/servlet/templateRoute.go +++ b/servlet/templateRoute.go @@ -8,7 +8,7 @@ import ( type ( TemplateRoute struct { *VinegarWebRoute - srv *VinegarHttpServlet + srv *VinegarWebServlet fileRoot string TemplateManager *TemplateManager UseCache bool @@ -16,7 +16,7 @@ type ( TemplateRouteHandlerFunc func(w http.ResponseWriter, r *http.Request, tm *TemplateManager) ) -func NewTemplateRoute(servlet *VinegarHttpServlet, urlPattern string, templatePath string, componentPath string, handler TemplateRouteHandlerFunc) *TemplateRoute { +func NewTemplateRoute(servlet *VinegarWebServlet, urlPattern string, templatePath string, componentPath string, handler TemplateRouteHandlerFunc) *TemplateRoute { defaultPrune := strings.Replace(urlPattern, ".*", "", -1) tm := NewTemplateManager(templatePath, componentPath) rootRoute := NewServletRoute(defaultPrune, createTemplateRouteFunction(tm, handler))