Compare commits
No commits in common. "1748b65cade426b4c13551556ae8d306e744c05e" and "5c7143ffd7d881c5c4448812ad82af2377277b12" have entirely different histories.
1748b65cad
...
5c7143ffd7
48
README.md
48
README.md
@ -1,48 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,10 +1,8 @@
|
|||||||
package servlet
|
package servlet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"geniuscartel.xyz/vinegar/vinegarUtil"
|
"geniuscartel.xyz/vinegar/vinegarUtil"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
@ -14,30 +12,6 @@ type (
|
|||||||
Routes []*VinegarWebRoute
|
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 {
|
VinegarWebRoute struct {
|
||||||
Pattern *regexp.Regexp
|
Pattern *regexp.Regexp
|
||||||
Handler VinegarHandlerFunction
|
Handler VinegarHandlerFunction
|
||||||
@ -54,25 +28,12 @@ func (r *VinegarWebRouter) RouteRequest(w http.ResponseWriter, req *http.Request
|
|||||||
path := req.URL.Path
|
path := req.URL.Path
|
||||||
for _, route := range r.Routes {
|
for _, route := range r.Routes {
|
||||||
if route.Pattern.MatchString(path) {
|
if route.Pattern.MatchString(path) {
|
||||||
log.Printf("SERVING: [%s]=>{%s}\n", path, route.Pattern.String())
|
//fmt.Printf("SERVING: [%s]=>{%s}\n", path, route.Pattern.String())
|
||||||
go route.Handler(w, req)
|
go route.Handler(w, req)
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notFoundHandler(w, req)
|
|
||||||
return errors.New("failed to match route for [" + path + "]")
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"geniuscartel.xyz/vinegar/vinegarUtil"
|
"geniuscartel.xyz/vinegar/vinegarUtil"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -23,6 +22,7 @@ type (
|
|||||||
//
|
//
|
||||||
// So when loading a JSON config, the "ConfigType" field must be
|
// So when loading a JSON config, the "ConfigType" field must be
|
||||||
// set to one of these constant values like "Text" or "Image".
|
// set to one of these constant values like "Text" or "Image".
|
||||||
|
|
||||||
ConfigType string
|
ConfigType string
|
||||||
|
|
||||||
// ConfigEntry defines a single route configuration entry.
|
// ConfigEntry defines a single route configuration entry.
|
||||||
@ -42,25 +42,6 @@ type (
|
|||||||
UseBuiltinCache bool
|
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 {
|
Config struct {
|
||||||
ListeningAddress string
|
ListeningAddress string
|
||||||
Routes []ConfigEntry
|
Routes []ConfigEntry
|
||||||
@ -169,7 +150,7 @@ func GenerateBlankConfig() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println("Generating a blank configuration file at ")
|
fmt.Println("Generating a blank configuration file at ")
|
||||||
ioutil.WriteFile(defaultTemplateFileName, content, 0755)
|
ioutil.WriteFile(defaultTemplateFileName, content, 0755)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,30 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
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 {
|
ApiRoute struct {
|
||||||
VinegarRoute *VinegarWebRoute
|
VinegarRoute *VinegarWebRoute
|
||||||
HttpMethodRoutes *map[string]VinegarHandlerFunction
|
HttpMethodRoutes *map[string]VinegarHandlerFunction
|
||||||
|
|||||||
@ -25,67 +25,9 @@ type ErrorResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type (
|
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 {
|
VinegarWebServlet struct {
|
||||||
Port string
|
Port string
|
||||||
Router VinegarWebRouter
|
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
|
ErrorRoutes map[int]*TemplateRoute
|
||||||
interrupts chan struct{}
|
interrupts chan struct{}
|
||||||
errors chan error
|
errors chan error
|
||||||
@ -165,10 +107,9 @@ func (r *VinegarWebRoute) Announce() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *VinegarWebServlet) 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) {
|
||||||
log.Printf("[%d][%s]. Rendering template for code %d with message: %s\n", code, req.URL.Path, code, msg)
|
fmt.Printf("[%d][%s]. Rendering template for code %d with message: %s\n", code, req.URL.Path, code, msg)
|
||||||
out, _ := json.Marshal(aErr)
|
out, _ := json.Marshal(aErr)
|
||||||
|
fmt.Println(string(out))
|
||||||
log.Println(string(out))
|
|
||||||
tmpl, exists := s.ErrorRoutes[code]
|
tmpl, exists := s.ErrorRoutes[code]
|
||||||
if exists {
|
if exists {
|
||||||
err := tmpl.TemplateManager.AddMixin("code", strconv.Itoa(code))
|
err := tmpl.TemplateManager.AddMixin("code", strconv.Itoa(code))
|
||||||
@ -183,11 +124,6 @@ func (s *VinegarWebServlet) SendError(w http.ResponseWriter, req *http.Request,
|
|||||||
}
|
}
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
bitties, err := tmpl.TemplateManager.RenderTemplate(fmt.Sprintf("%d.html", 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))
|
_, err = w.Write([]byte(bitties))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, msg, code)
|
http.Error(w, msg, code)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package servlet
|
package servlet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -37,11 +38,10 @@ func TestServletServeTraffic(t *testing.T) {
|
|||||||
srv.Router.AddRoute(&VinegarWebRoute{
|
srv.Router.AddRoute(&VinegarWebRoute{
|
||||||
Pattern: regexp.MustCompile("/test"),
|
Pattern: regexp.MustCompile("/test"),
|
||||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("We got into the handler\n")
|
fmt.Printf("We got into the handler\n")
|
||||||
msg := []byte("hello")
|
msg := []byte("hello")
|
||||||
l, err := w.Write(msg)
|
l, err := w.Write(msg)
|
||||||
|
fmt.Printf("Wrote %d bytes\n", l)
|
||||||
log.Printf("Wrote %d bytes\n", l)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
srv.errors <- err
|
srv.errors <- err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,41 +12,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
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 {
|
TemplateManager struct {
|
||||||
templatePath string
|
templatePath string
|
||||||
componentPath string
|
componentPath string
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package vinegarUtil
|
package vinegarUtil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -60,7 +61,7 @@ func NewSingleFileCache(pathlike string) (*SingleFileCache, error) {
|
|||||||
func (l *Lru) Get(key string) (*LruEntry, bool) {
|
func (l *Lru) Get(key string) (*LruEntry, bool) {
|
||||||
entry, exists := (*l.entries)[key]
|
entry, exists := (*l.entries)[key]
|
||||||
if exists && entry.expires.Before(time.Now()) {
|
if exists && entry.expires.Before(time.Now()) {
|
||||||
log.Println("Cache miss due to expired content")
|
fmt.Println("Cache miss due to expired content")
|
||||||
err := entry.Reload()
|
err := entry.Reload()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user