Brijesh's Git Server — toolkit @ 07f7705d52a615e47c3440f848a64abd85da22b1

my attempt at building my own web framework so I feel more confident when using chi

re-releasing pacakge
Brijesh ops@brijesh.dev
Thu, 29 Aug 2024 17:44:42 +0530
commit

07f7705d52a615e47c3440f848a64abd85da22b1

A .gitignore

@@ -0,0 +1,1 @@

+test
A LICENSE

@@ -0,0 +1,22 @@

+MIT License + +Copyright (c) 2024 Brijesh Wawdhane + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +
A README.md

@@ -0,0 +1,72 @@

+# Toolkit + +Toolkit is a Go library providing utilities for API development. + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) +- [Features](#features) + +## Installation + +To install Toolkit, use the following command: + +```bash +go get brijesh.dev/toolkit +``` + +## Usage + +Here's a quick example of how to use Toolkit: + +```go +package main + +import ( + "net/http" + "time" + + "brijesh.dev/toolkit/middleware" + "brijesh.dev/toolkit/router" +) + +type response struct { + Message string `json:"message"` + Error error `json:"error"` + Data interface{} `json:"data"` + RequestID string `json:"request_id"` +} + +func main() { + r := router.NewRouter() + + r.Use(middleware.Logger) + r.Use(middleware.RequestIDMiddleware) + r.Use(middleware.RateLimit(2, time.Second)) + + r.GET("/health", healthCheckHandler) + + http.ListenAndServe(":8080", r) +} + +func healthCheckHandler(w http.ResponseWriter, r *http.Request) { + response := response{ + Message: "Okay", + Error: nil, + Data: nil, + RequestID: middleware.GetRequestID(r), + } + router.SendResponse(w, http.StatusOK, response) +} +``` + +## Features + +- **Router**: A lightweight wrapper around net/http, providing an intuitive API for defining routes and middleware. +- **Middleware**: + - Logger: Logs incoming requests with details like method, path, and response time. + - Rate Limiter: Implements rate limiting to protect your API from abuse. + - Request ID: Assigns a unique ID to each request for easier tracking and debugging. +- **BUID (Brijesh's Unique Identifier)**: A custom unique identifier generator, similar to UUID but with its own algorithm. +- **Benchmarking**: A package to easily benchmark your functions and measure performance.
A benchmark/benchmark.go

@@ -0,0 +1,25 @@

+package benchmark + +import ( + "fmt" + "time" +) + +func BenchmarkFunction(fn func(), count int, rounds int) { + var totalDuration time.Duration + + for r := 0; r < rounds; r++ { + start := time.Now() + + for i := 0; i < count; i++ { + fn() + } + + duration := time.Since(start) + totalDuration += duration + } + + averageDuration := totalDuration / time.Duration(rounds) + fmt.Printf("\nAverage time over %d rounds: %v\n", rounds, averageDuration) + fmt.Printf("Average time per operation: %v\n", averageDuration/time.Duration(count)) +}
A buid/buid.go

@@ -0,0 +1,36 @@

+package buid + +import ( + "math/rand" + "time" +) + +const ( + base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + buidLength = 18 + randomCharsLength = 11 +) + +func GenerateBUID() string { + // Pre-allocate byte slice for result + result := make([]byte, buidLength) + + // Convert the current Unix timestamp to base32 + now := time.Now().UTC().Unix() + pos := buidLength - randomCharsLength + + for now > 0 { + pos-- + result[pos] = base32Alphabet[now&31] + now /= 32 + } + + // Generate the random part directly into the result slice + for i := buidLength - randomCharsLength; i < buidLength; i++ { + // n, _ := rand.Int(rand.Reader, big.NewInt(32)) // Ignoring the error as before + // result[i] = base32Alphabet[n.Int64()] + result[i] = base32Alphabet[rand.Intn(32)] + } + + return string(result[pos:]) +}
A buid/buid.md

@@ -0,0 +1,58 @@

+# BUID (Brijesh's Unique Identifier) Specification v1.0 + +<!--toc:start--> + +- [Introduction](#introduction) +- [Who Should Use This](#who-should-use-this) +- [Structure](#structure) + - [Components](#components) +- [Example](#example) +- [Comparison with UUID](#comparison-with-uuid) +- [Shortcomings](#shortcomings) +- [Version](#version) +<!--toc:end--> + +## Introduction + +I created BUID for fun and to address the need for a compact, human-readable, and sortable unique identifier. + +## Who Should Use This + +BUID is ideal for systems that require: + +- Compact unique identifiers (shorter than UUID) +- Rough chronological sorting capability +- URL-safe and human-readable format + +## Structure + +- Total length: 18 characters +- Encoding: Base32 (A-Z and 2-7) + +### Components + +1. **Timestamp** (first 7 characters): +2. **Random component** (next 11 characters): + +## Example + +`BTMSEM5RXDBPOQLHXC` + +## Comparison with UUID + +| Feature | BUID | UUID (v4) | +| ----------------------- | ----------------- | ---------------------- | +| Length | 18 characters | 36 characters | +| Character set | Base32 (A-Z, 2-7) | Hexadecimal (0-9, A-F) | +| Sortability | Sortable\* | Not sortable | +| Randomness | 2^55 / second | 2^128 | +| Double-click selectable | Yes | No | + +## Shortcomings + +- The timestamp is generated using the local system time, converted to UTC. Users should be aware that strict chronological ordering of BUIDs across different systems is not guaranteed. +- As BUID is only 18 characters long, it's randomness will fall short if usage reaches over hundred thousand IDs generated per second. + +## Version + +Specification Version: 1.0
A go.mod

@@ -0,0 +1,3 @@

+module brijesh.dev/toolkit + +go 1.22.4
A middleware/logger.go

@@ -0,0 +1,84 @@

+package middleware + +import ( + "fmt" + "log" + "net/http" + "time" +) + +// customResponseWriter wraps the standard http.ResponseWriter to capture the status code +type customResponseWriter struct { + http.ResponseWriter + statusCode int +} + +func newCustomResponseWriter(w http.ResponseWriter) *customResponseWriter { + // Default status code to 200 OK + return &customResponseWriter{w, http.StatusOK} +} + +func (rw *customResponseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +// Logger middleware logs request details and uses different log levels based on the status code +func Logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Start time + start := time.Now() + + // Wrap the ResponseWriter to capture the status code + crw := newCustomResponseWriter(w) + + // Serve the request + next.ServeHTTP(crw, r) + + // Determine the log level based on the status code + duration := time.Since(start) + statusCode := crw.statusCode + + // Log entry with timestamp, log level, and request details + logEntry := fmt.Sprintf("%s method=%s url=%s status=%d duration=%s", + getLogLevel(statusCode), + r.Method, + r.URL.Path, + statusCode, + duration, + ) + + // Log the entry + log.Println(logEntry) + }) +} + +// getLogLevel returns the log level based on the status code +func getLogLevel(statusCode int) string { + switch { + case statusCode >= 500: + return colorize("ERROR", "red") + case statusCode >= 400: + return colorize("WARN", "yellow") + default: + return colorize("INFO", "blue") + } +} + +// colorize returns the text wrapped in ANSI escape codes for coloring +func colorize(text, color string) string { + var colorCode string + switch color { + case "red": + colorCode = "\033[31m" + case "green": + colorCode = "\033[32m" + case "yellow": + colorCode = "\033[33m" + case "blue": + colorCode = "\033[1;34m" + default: + colorCode = "\033[0m" // Reset color + } + return fmt.Sprintf("%s%s\033[0m", colorCode, text) +}
A middleware/rate_limiter.go

@@ -0,0 +1,56 @@

+package middleware + +import ( + "net/http" + "sync" + "time" +) + +type Middleware func(http.Handler) http.Handler + +type rateLimiter struct { + resetTime time.Time + requests int + maxRequests int + resetDuration time.Duration + mu sync.Mutex +} + +func newRateLimiter(maxRequests int, duration time.Duration) *rateLimiter { + return &rateLimiter{ + maxRequests: maxRequests, + resetDuration: duration, + resetTime: time.Now().Add(duration), + } +} + +func (rl *rateLimiter) allow() bool { + rl.mu.Lock() + defer rl.mu.Unlock() + + if time.Now().After(rl.resetTime) { + rl.requests = 0 + rl.resetTime = time.Now().Add(rl.resetDuration) + } + + if rl.requests >= rl.maxRequests { + return false + } + + rl.requests++ + return true +} + +func RateLimit(maxRequests int, duration time.Duration) Middleware { + limiter := newRateLimiter(maxRequests, duration) + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !limiter.allow() { + http.Error(w, "Too many requests", http.StatusTooManyRequests) + return + } + next.ServeHTTP(w, r) + }) + } +}
A middleware/request_id.go

@@ -0,0 +1,27 @@

+package middleware + +import ( + "context" + "errors" + "net/http" + + "brijesh.dev/toolkit/buid" +) + +func RequestIDMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestID := buid.GenerateBUID() + + ctx := context.WithValue(r.Context(), "requestID", requestID) + w.Header().Set("X-Request-ID", requestID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func GetRequestID(r *http.Request) string { + requestID := r.Context().Value("requestID").(string) + if requestID == "" { + panic(errors.New("calling GetRequestID without using RequestIDMiddleware")) + } + return requestID +}
A router/router.go

@@ -0,0 +1,77 @@

+package router + +import ( + "net/http" + + "brijesh.dev/toolkit/middleware" +) + +type router struct { + mux *http.ServeMux + middleware []middleware.Middleware +} + +func NewRouter() *router { + return &router{ + mux: http.NewServeMux(), + middleware: []middleware.Middleware{}, + } +} + +func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + finalHandler := http.Handler(r.mux) + for i := len(r.middleware) - 1; i >= 0; i-- { + finalHandler = r.middleware[i](finalHandler) + } + finalHandler.ServeHTTP(w, req) +} + +func (r *router) Use(mw middleware.Middleware) { + r.middleware = append(r.middleware, mw) +} + +func (r *router) Handle(method, pattern string, handler http.HandlerFunc) { + r.mux.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) { + if req.Method != method { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + handler(w, req) + }) +} + +func (r *router) GET(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodGet, pattern, handler) +} + +func (r *router) POST(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodPost, pattern, handler) +} + +func (r *router) PUT(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodPut, pattern, handler) +} + +func (r *router) DELETE(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodDelete, pattern, handler) +} + +func (r *router) PATCH(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodPatch, pattern, handler) +} + +func (r *router) HEAD(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodHead, pattern, handler) +} + +func (r *router) OPTIONS(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodOptions, pattern, handler) +} + +func (r *router) CONNECT(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodConnect, pattern, handler) +} + +func (r *router) TRACE(pattern string, handler http.HandlerFunc) { + r.Handle(http.MethodTrace, pattern, handler) +}
A router/send_json.go

@@ -0,0 +1,12 @@

+package router + +import ( + "encoding/json" + "net/http" +) + +func SendResponse(w http.ResponseWriter, status int, response interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(response) +}