make params an any slice to support automatic int conversion, add readme

This commit is contained in:
Sky Johnson 2025-08-15 14:50:14 -05:00
parent 8944c20394
commit e100f2d56b
9 changed files with 440 additions and 19 deletions

421
README.md Normal file
View File

@ -0,0 +1,421 @@
# 🍣 Sushi
A fast, raw, tasty framework for simplifying basic web apps!
## Quick Start
```go
package main
import (
"git.sharkk.net/Sharkk/Sushi"
"git.sharkk.net/Sharkk/Sushi/session"
)
func main() {
app := sushi.New()
// Initialize sessions
session.InitSessions("sessions.json")
app.Get("/", func(ctx sushi.Ctx, params []any) {
sushi.SendHTML(ctx, "<h1>Hello Sushi!</h1>")
})
app.Listen(":8080")
}
```
## Routing
```go
// Basic routes
app.Get("/users/:id", getUserHandler)
app.Post("/users", createUserHandler)
app.Put("/users/:id", updateUserHandler)
app.Delete("/users/:id", deleteUserHandler)
// Wildcards
app.Get("/files/*path", serveFilesHandler)
// Route groups
api := app.Group("/api/v1")
api.Get("/users", listUsersHandler)
api.Post("/users", createUserHandler)
```
## Parameters
URL parameters are automatically converted to the appropriate type:
```go
// Numeric parameters become integers
app.Get("/users/:id", func(ctx sushi.Ctx, params []any) {
userID := params[0].(int) // /users/123 -> 123
// ...
})
// String parameters stay strings
app.Get("/users/:name", func(ctx sushi.Ctx, params []any) {
name := params[0].(string) // /users/john -> "john"
// ...
})
// Mixed types
app.Get("/users/:id/posts/:slug", func(ctx sushi.Ctx, params []any) {
userID := params[0].(int) // 123
slug := params[1].(string) // "my-post"
// ...
})
```
## Response Helpers
```go
func myHandler(ctx sushi.Ctx, params []any) {
// JSON responses
sushi.SendJSON(ctx, map[string]string{"message": "success"})
// HTML responses
sushi.SendHTML(ctx, "<h1>Welcome</h1>")
// Text responses
sushi.SendText(ctx, "Plain text")
// Error responses
sushi.SendError(ctx, 404, "Not Found")
// Redirects
sushi.SendRedirect(ctx, "/login")
// Status only
sushi.SendStatus(ctx, 204)
}
```
## Middleware
```go
// Custom middleware
func loggingMiddleware() sushi.Middleware {
return func(ctx sushi.Ctx, params []any, next func()) {
println("Request:", string(ctx.Method()), string(ctx.Path()))
next()
println("Status:", ctx.Response.StatusCode())
}
}
app.Use(loggingMiddleware())
// Group middleware
admin := app.Group("/admin")
admin.Use(auth.RequireAuth("/login"))
```
## Authentication Workflow
### 1. Setup Password Hashing
```go
import "git.sharkk.net/Sharkk/Sushi/password"
// Hash password for storage
hashedPassword := password.HashPassword("userpassword123")
// Verify password during login
isValid, err := password.VerifyPassword("userpassword123", hashedPassword)
```
### 2. User Structure
```go
type User struct {
ID int `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
Password string `json:"password"` // Store hashed password
}
// User lookup function for auth middleware
func getUserByID(userID int) any {
// Query your database for user by ID
// Return nil if not found
return &User{ID: userID, Email: "user@example.com"}
}
```
### 3. Session & Auth Middleware
```go
import (
"git.sharkk.net/Sharkk/Sushi/session"
"git.sharkk.net/Sharkk/Sushi/auth"
)
func main() {
app := sushi.New()
// Initialize sessions
session.InitSessions("sessions.json")
// Add session middleware
app.Use(session.Middleware())
// Add auth middleware with user lookup
app.Use(auth.Middleware(getUserByID))
// Public routes
app.Get("/login", loginPageHandler)
app.Post("/login", loginHandler)
app.Post("/logout", logoutHandler)
// Protected routes
protected := app.Group("/dashboard")
protected.Use(auth.RequireAuth("/login"))
protected.Get("/", dashboardHandler)
}
```
### 4. Login Handler
```go
func loginHandler(ctx sushi.Ctx, params []string) {
email := string(ctx.PostArgs().Peek("email"))
password := string(ctx.PostArgs().Peek("password"))
// Find user by email/username
user := findUserByEmail(email)
if user == nil {
sushi.SendError(ctx, 401, "Invalid credentials")
return
}
// Verify password
isValid, err := password.VerifyPassword(password, user.Password)
if err != nil || !isValid {
sushi.SendError(ctx, 401, "Invalid credentials")
return
}
// Log the user in
auth.Login(ctx, user.ID, user)
sushi.SendRedirect(ctx, "/dashboard")
}
```
### 5. Logout Handler
```go
func logoutHandler(ctx sushi.Ctx, params []string) {
auth.Logout(ctx)
sushi.SendRedirect(ctx, "/")
}
```
### 6. Getting Current User
```go
func dashboardHandler(ctx sushi.Ctx, params []string) {
user := auth.GetCurrentUser(ctx).(*User)
html := fmt.Sprintf("<h1>Welcome, %s!</h1>", user.Username)
sushi.SendHTML(ctx, html)
}
```
## CSRF Protection
```go
import "git.sharkk.net/Sharkk/Sushi/csrf"
// Add CSRF middleware to forms
app.Use(csrf.Middleware())
// In your form template
func loginPageHandler(ctx sushi.Ctx, params []string) {
csrfField := csrf.CSRFHiddenField(ctx)
html := fmt.Sprintf(`
<form method="POST" action="/login">
%s
<input type="email" name="email" required>
<input type="password" name="password" required>
<button type="submit">Login</button>
</form>
`, csrfField)
sushi.SendHTML(ctx, html)
}
```
## Static Files
```go
// Serve static files
app.Get("/static/*path", sushi.Static("./public"))
// Serve single file
app.Get("/favicon.ico", sushi.StaticFile("./public/favicon.ico"))
// Embedded files
files := map[string][]byte{
"/style.css": cssData,
"/app.js": jsData,
}
app.Get("/assets/*path", sushi.StaticEmbed(files))
```
## Sessions
```go
func someHandler(ctx sushi.Ctx, params []string) {
sess := session.GetCurrentSession(ctx)
// Set session data
sess.Set("user_preference", "dark_mode")
// Get session data
if pref, exists := sess.Get("user_preference"); exists {
preference := pref.(string)
}
// Flash messages (one-time)
sess.SetFlash("success", "Profile updated!")
// Get flash message
message := sess.GetFlashMessage("success")
}
```
## Server Configuration
```go
app := sushi.New(sushi.ServerOptions{
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxRequestBodySize: 10 * 1024 * 1024, // 10MB
})
// TLS
app.ListenTLS(":443", "cert.pem", "key.pem")
```
## Complete Auth Example
```go
package main
import (
"fmt"
sushi "git.sharkk.net/Sharkk/Sushi"
"git.sharkk.net/Sharkk/Sushi/auth"
"git.sharkk.net/Sharkk/Sushi/csrf"
"git.sharkk.net/Sharkk/Sushi/password"
"git.sharkk.net/Sharkk/Sushi/session"
)
type User struct {
ID int
Email string
Password string
}
var users = map[int]*User{
1: {ID: 1, Email: "admin@example.com", Password: password.HashPassword("admin123")},
}
func getUserByID(userID int) any {
return users[userID]
}
func findUserByEmail(email string) *User {
for _, user := range users {
if user.Email == email {
return user
}
}
return nil
}
func main() {
app := sushi.New()
session.InitSessions("sessions.json")
app.Use(session.Middleware())
app.Use(auth.Middleware(getUserByID))
// Public routes
app.Get("/", homeHandler)
app.Get("/login", loginPageHandler)
app.Post("/login", loginHandler)
// Protected routes
protected := app.Group("/dashboard")
protected.Use(auth.RequireAuth("/login"))
protected.Use(csrf.Middleware())
protected.Get("/", dashboardHandler)
protected.Post("/logout", logoutHandler)
app.Listen(":8080")
}
func homeHandler(ctx sushi.Ctx, params []string) {
if auth.IsAuthenticated(ctx) {
sushi.SendRedirect(ctx, "/dashboard")
return
}
sushi.SendHTML(ctx, `<a href="/login">Login</a>`)
}
func loginPageHandler(ctx sushi.Ctx, params []string) {
html := fmt.Sprintf(`
<form method="POST" action="/login">
%s
<input type="email" name="email" placeholder="Email" required><br>
<input type="password" name="password" placeholder="Password" required><br>
<button type="submit">Login</button>
</form>
`, csrf.CSRFHiddenField(ctx))
sushi.SendHTML(ctx, html)
}
func loginHandler(ctx sushi.Ctx, params []string) {
email := string(ctx.PostArgs().Peek("email"))
pass := string(ctx.PostArgs().Peek("password"))
user := findUserByEmail(email)
if user == nil {
sushi.SendError(ctx, 401, "Invalid credentials")
return
}
if valid, _ := password.VerifyPassword(pass, user.Password); !valid {
sushi.SendError(ctx, 401, "Invalid credentials")
return
}
auth.Login(ctx, user.ID, user)
sushi.SendRedirect(ctx, "/dashboard")
}
func dashboardHandler(ctx sushi.Ctx, params []string) {
user := auth.GetCurrentUser(ctx).(*User)
html := fmt.Sprintf(`
<h1>Welcome, %s!</h1>
<form method="POST" action="/logout">
%s
<button type="submit">Logout</button>
</form>
`, user.Email, csrf.CSRFHiddenField(ctx))
sushi.SendHTML(ctx, html)
}
func logoutHandler(ctx sushi.Ctx, params []string) {
auth.Logout(ctx)
sushi.SendRedirect(ctx, "/")
}
```

View File

@ -10,7 +10,7 @@ const UserCtxKey = "user"
// Middleware adds authentication handling
func Middleware(userLookup func(int) any) sushi.Middleware {
return func(ctx sushi.Ctx, params []string, next func()) {
return func(ctx sushi.Ctx, params []any, next func()) {
sess := session.GetCurrentSession(ctx)
if sess != nil && sess.UserID > 0 && userLookup != nil {
user := userLookup(sess.UserID)
@ -32,7 +32,7 @@ func RequireAuth(redirectPath ...string) sushi.Middleware {
redirect = redirectPath[0]
}
return func(ctx sushi.Ctx, params []string, next func()) {
return func(ctx sushi.Ctx, params []any, next func()) {
if !IsAuthenticated(ctx) {
ctx.Redirect(redirect, fasthttp.StatusFound)
return
@ -48,7 +48,7 @@ func RequireGuest(redirectPath ...string) sushi.Middleware {
redirect = redirectPath[0]
}
return func(ctx sushi.Ctx, params []string, next func()) {
return func(ctx sushi.Ctx, params []any, next func()) {
if IsAuthenticated(ctx) {
ctx.Redirect(redirect, fasthttp.StatusFound)
return

View File

@ -119,7 +119,7 @@ func ValidateFormCSRFToken(ctx sushi.Ctx) bool {
// Middleware returns middleware that automatically validates CSRF tokens
func Middleware() sushi.Middleware {
return func(ctx sushi.Ctx, params []string, next func()) {
return func(ctx sushi.Ctx, params []any, next func()) {
method := string(ctx.Method())
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {

6
fs.go
View File

@ -50,7 +50,7 @@ func StaticFS(fsOptions StaticOptions) Handler {
fsHandler := fs.NewRequestHandler()
return func(ctx Ctx, params []string) {
return func(ctx Ctx, params []any) {
fsHandler(ctx)
}
}
@ -62,14 +62,14 @@ func Static(root string) Handler {
// StaticFile serves a single file
func StaticFile(filePath string) Handler {
return func(ctx Ctx, params []string) {
return func(ctx Ctx, params []any) {
fasthttp.ServeFile(ctx, filePath)
}
}
// StaticEmbed creates a handler for embedded files
func StaticEmbed(files map[string][]byte) Handler {
return func(ctx Ctx, params []string) {
return func(ctx Ctx, params []any) {
requestPath := string(ctx.Path())
// Try to find the file

View File

@ -22,7 +22,7 @@ type Router struct {
patch *node
delete *node
middleware []Middleware
paramsBuffer []string
paramsBuffer []any
}
type Group struct {
@ -40,7 +40,7 @@ func NewRouter() *Router {
patch: &node{},
delete: &node{},
middleware: []Middleware{},
paramsBuffer: make([]string, 64),
paramsBuffer: make([]any, 64),
}
}
@ -145,7 +145,7 @@ func applyMiddleware(h Handler, mw []Middleware) Handler {
return h
}
return func(ctx Ctx, params []string) {
return func(ctx Ctx, params []any) {
var index int
var next func()
@ -229,7 +229,7 @@ func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) e
}
// Lookup finds a handler matching method and path
func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
func (r *Router) Lookup(method, path string) (Handler, []any, bool) {
root := r.methodNode(method)
if root == nil {
return nil, nil, false
@ -240,7 +240,7 @@ func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
buffer := r.paramsBuffer
if cap(buffer) < int(root.maxParams) {
buffer = make([]string, root.maxParams)
buffer = make([]any, root.maxParams)
r.paramsBuffer = buffer
}
buffer = buffer[:0]
@ -253,7 +253,7 @@ func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
return h, buffer[:paramCount], true
}
func match(current *node, path string, start int, params *[]string) (Handler, int, bool) {
func match(current *node, path string, start int, params *[]any) (Handler, int, bool) {
paramCount := 0
for _, c := range current.children {

View File

@ -4,7 +4,7 @@ import sushi "git.sharkk.net/Sharkk/Sushi"
// Middleware provides session handling
func Middleware() sushi.Middleware {
return func(ctx sushi.Ctx, params []string, next func()) {
return func(ctx sushi.Ctx, params []any, next func()) {
sessionID := sushi.GetCookie(ctx, SessionCookieName)
var sess *Session

View File

@ -8,7 +8,7 @@ import (
"github.com/valyala/fasthttp"
)
func (h Handler) Serve(ctx Ctx, params []string) {
func (h Handler) Serve(ctx Ctx, params []any) {
h(ctx, params)
}
@ -20,7 +20,7 @@ func IsHTTPS(ctx Ctx) bool {
// StandardHandler adapts a standard fasthttp.RequestHandler to the router's Handler
func StandardHandler(handler fasthttp.RequestHandler) Handler {
return func(ctx Ctx, _ []string) {
return func(ctx Ctx, _ []any) {
handler(ctx)
}
}

View File

@ -11,7 +11,7 @@ const RequestTimerKey = "request_start_time"
// Middleware adds request timing functionality
func Middleware() sushi.Middleware {
return func(ctx sushi.Ctx, params []string, next func()) {
return func(ctx sushi.Ctx, params []any, next func()) {
startTime := time.Now()
ctx.SetUserValue(RequestTimerKey, startTime)
next()

View File

@ -3,5 +3,5 @@ package sushi
import "github.com/valyala/fasthttp"
type Ctx = *fasthttp.RequestCtx
type Handler func(ctx Ctx, params []string)
type Middleware func(ctx Ctx, params []string, next func())
type Handler func(ctx Ctx, params []any)
type Middleware func(ctx Ctx, params []any, next func())