Compare commits

...

2 Commits

Author SHA1 Message Date
dtookey
1748b65cad updated a bunch of docs 2023-08-01 13:34:08 -04:00
dtookey
0ae5afa452 added readme 2023-08-01 12:53:09 -04:00
8 changed files with 242 additions and 14 deletions

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# Vinegar
### Vinegar is a simple web application framework written in Go.
## Features
Routing with support for static files, templates, APIs
Template rendering
Caching
Gzip compression
Custom error pages
Getting Started
Prerequisites
Go 1.18+
### Installation
```go get geniuscartel.xyz/vinegar```
### Usage
Create a config.json file to define routes and options
### Import Vinegar and create a new server instance:
```
import "geniuscartel.xyz/vinegar"
func main(){
server := vinegar.NewServer(config)
Add handlers for API routes:
api := server.NewApiRoute("/api")
api.Get(handleGet)
Start the server:
server.Start(":8080")
}
```
See the examples folder for more usage examples.
## Configuration
The config.json file defines the routes and options for Vinegar. It supports the following route types:
* static - Map a URL pattern to a static file or directory
* template - Render a Go template on a route
* api - Add API routes with custom handlers
* See config_example.json for a sample configuration file.
## License
Vinegar is closed source. Copyright 2023 David Tookey. All rights reserved

View File

@ -1,8 +1,10 @@
package servlet
import (
"encoding/json"
"errors"
"geniuscartel.xyz/vinegar/vinegarUtil"
"log"
"net/http"
"regexp"
)
@ -12,6 +14,30 @@ type (
Routes []*VinegarWebRoute
}
// VinegarWebRoute defines a single route in the router.
// It contains the following fields:
//
//Pattern - A regexp.Regexp instance that matches against the request URL path.
//
//Handler - A VinegarHandlerFunction to call when the route matches a request.
//
//Cache - An optional vinegarUtil.Cache instance to enable caching for this route.
//
//VinegarWebRoute is used to define custom routes to handle requests in the
//VinegarWebRouter. Routes should be added to the router using:
//
// router.AddRoute(route)
//
//The router will match incoming requests against route patterns in order.
//When a match is found, the Handler is executed to handle the request.
//
//Example usage:
//
// route := VinegarWebRoute{
// Pattern: regexp.MustCompile("/users"),
// Handler: myUserHandler,
// }
// router.AddRoute(&route)
VinegarWebRoute struct {
Pattern *regexp.Regexp
Handler VinegarHandlerFunction
@ -28,12 +54,25 @@ func (r *VinegarWebRouter) RouteRequest(w http.ResponseWriter, req *http.Request
path := req.URL.Path
for _, route := range r.Routes {
if route.Pattern.MatchString(path) {
//fmt.Printf("SERVING: [%s]=>{%s}\n", path, route.Pattern.String())
log.Printf("SERVING: [%s]=>{%s}\n", path, route.Pattern.String())
go route.Handler(w, req)
return nil
}
}
notFoundHandler(w, req)
return errors.New("failed to match route for [" + path + "]")
}
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
errorResponse := ErrorResponse{
Code: http.StatusNotFound,
Message: "The requested resource could not be found.",
}
jsonBytes, _ := json.Marshal(errorResponse)
w.Header().Add("Content-Type", "application/json")
w.Write(jsonBytes)
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"geniuscartel.xyz/vinegar/vinegarUtil"
"io/ioutil"
"log"
)
type (
@ -22,7 +23,6 @@ type (
//
// So when loading a JSON config, the "ConfigType" field must be
// set to one of these constant values like "Text" or "Image".
ConfigType string
// ConfigEntry defines a single route configuration entry.
@ -42,6 +42,25 @@ type (
UseBuiltinCache bool
}
//Config holds the configuration for creating a VinegarWebServlet instance
//from a JSON file. It contains:
//
//ListeningAddress - The address to listen on as a string like ":8080"
//
//Routes - A slice of ConfigEntry structs, each representing a route.
//
//It is loaded from JSON like:
//
//
// config, err := LoadConfig(configFilePath)
//
//Then used when creating the servlet:
//
// servlet := NewServlet(conf.ListeningAddress)
// // Add routes based on conf.Routes
//
//So Config allows declaring all VinegarWebServlet routes, options, etc
//in a JSON file for easy loading.
Config struct {
ListeningAddress string
Routes []ConfigEntry
@ -150,7 +169,7 @@ func GenerateBlankConfig() error {
if err != nil {
return err
}
fmt.Println("Generating a blank configuration file at ")
log.Println("Generating a blank configuration file at ")
ioutil.WriteFile(defaultTemplateFileName, content, 0755)
return nil
}

View File

@ -7,6 +7,30 @@ import (
)
type (
//ApiRoute provides a way to define API routes that handle multiple HTTP methods.
//
//It contains:
//
//VinegarRoute - The base VinegarWebRoute that defines the URL pattern to match.
//
//HttpMethodRoutes - A pointer to a map of HTTP method string to handler function.
//
//Usage:
//
//1. Create a new ApiRoute, providing a VinegarHttpServlet and URL pattern:
//
// route := NewApiRoute(serv, "/api/users")
//
//2. Add handler functions for each supported HTTP method:
//
// route.RegisterHttpMethodHandler(http.MethodGet, handleGet)
// route.RegisterHttpMethodHandler(http.MethodPost, handlePost)
//
//3. The route will now match the pattern "/api/users" and call the right
// handler function based on the HTTP method used in the request.
//
//4. If an unsupported HTTP method is used, a 404 error will be returned.
ApiRoute struct {
VinegarRoute *VinegarWebRoute
HttpMethodRoutes *map[string]VinegarHandlerFunction

View File

@ -25,9 +25,67 @@ type ErrorResponse struct {
}
type (
// VinegarWebServlet is the main server struct that handles HTTP requests and routing.
// It contains the following key fields:
//
//Port - The TCP port to listen on for incoming requests. Should be a string like ":8080".
//
//Router - A VinegarWebRouter instance that handles routing requests to handlers.
//
//ErrorRoutes - A map of status code to TemplateRoute for custom error pages.
//
//interrupts - A channel used to signal shutdown/interrupts to the server.
//
//errors - A channel that the server writes errors to.
//
//The main methods are:
//
//ServeHTTP - Handles all incoming requests. Calls Router.RouteRequest to handle routing.
//
//AddErrorRoute - Adds a custom error route for the given status code.
//
//Start - Starts the HTTP server listening on Port. Returns any errors.
//
//Shutdown - Shuts down the server by sending a signal on interrupts.
//
//SendError - Helper to send an error response with custom status code and error template.
//
//Typical usage:
//
// srv := NewServlet(":8080")
// srv.AddErrorRoute(404, notFoundRoute)
// srv.Router.AddRoute(someRoute)
// srv.Start()
// // ...
// srv.Shutdown()
VinegarWebServlet struct {
Port string
Router VinegarWebRouter
// ErrorRoutes is a map that stores TemplateRoute pointers
// for different HTTP status code error pages.
//
// It gets populated when custom error routes are defined via AddErrorRoute.
// For example:
//
// err404 := NewTemplateRoute(/*...*/)
// srv.AddErrorRoute(404, err404)
//
// This will add err404 to ErrorRoutes mapped to 404.
//
// Routes can then be rendered like:
//
// tmpl, exists := s.ErrorRoutes[404]
// if exists {
// // render tmpl
// }
//
// So ErrorRoutes provides a lookup from status code
// to the associated custom error route.
//
// It allows easily rendering custom error pages
// for different status codes.
ErrorRoutes map[int]*TemplateRoute
interrupts chan struct{}
errors chan error
@ -107,9 +165,10 @@ func (r *VinegarWebRoute) Announce() {
}
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)
log.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))
log.Println(string(out))
tmpl, exists := s.ErrorRoutes[code]
if exists {
err := tmpl.TemplateManager.AddMixin("code", strconv.Itoa(code))
@ -124,6 +183,11 @@ func (s *VinegarWebServlet) SendError(w http.ResponseWriter, req *http.Request,
}
w.WriteHeader(code)
bitties, err := tmpl.TemplateManager.RenderTemplate(fmt.Sprintf("%d.html", code))
if err != nil {
http.Error(w, msg, code)
return
}
_, err = w.Write([]byte(bitties))
if err != nil {
http.Error(w, msg, code)

View File

@ -1,7 +1,6 @@
package servlet
import (
"fmt"
"io"
"log"
"net/http"
@ -38,10 +37,11 @@ func TestServletServeTraffic(t *testing.T) {
srv.Router.AddRoute(&VinegarWebRoute{
Pattern: regexp.MustCompile("/test"),
Handler: func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("We got into the handler\n")
log.Printf("We got into the handler\n")
msg := []byte("hello")
l, err := w.Write(msg)
fmt.Printf("Wrote %d bytes\n", l)
log.Printf("Wrote %d bytes\n", l)
if err != nil {
srv.errors <- err
}

View File

@ -12,6 +12,41 @@ import (
)
type (
//TemplateManager handles loading and rendering templates.
//It contains the following fields:
//
//templatePath - The base path to load template files from.
//
//componentPath - The base path to load template components from.
//
//templates - A map of template name to parsed *template.Template.
//
//components - A map of component name to template.HTML content.
//
//mixins - A map of mixin name to content string.
//
//Templates - A slice of all template names that were loaded.
//
//The main methods are:
//
//NewTemplateManager - Constructor to create a new instance.
//
//RenderTemplate - Renders a template by name and returns the output.
//
//GetComponent - Fetches a component by name.
//
//AddMixin - Adds a new mixin value by name.
//
//AddMixinFromFile - Loads a mixin from a file.
//
//RenderAllToFile - Renders all templates to files in a directory.
//
//Typical usage:
//
// tm := NewTemplateManager("templates", "components")
// output := tm.RenderTemplate("index.tmpl")
// tm.AddMixin("title", "My Page")
TemplateManager struct {
templatePath string
componentPath string

View File

@ -1,7 +1,6 @@
package vinegarUtil
import (
"fmt"
"log"
"strings"
"time"
@ -61,7 +60,7 @@ func NewSingleFileCache(pathlike string) (*SingleFileCache, error) {
func (l *Lru) Get(key string) (*LruEntry, bool) {
entry, exists := (*l.entries)[key]
if exists && entry.expires.Before(time.Now()) {
fmt.Println("Cache miss due to expired content")
log.Println("Cache miss due to expired content")
err := entry.Reload()
if err != nil {
return nil, false