1 Commits
v0.3.2 ... mux

Author SHA1 Message Date
Colin Henry
19860f713c WIP: http mux based on match by ben hoyt 2022-03-28 19:34:11 -07:00
25 changed files with 311 additions and 266 deletions

View File

@@ -1,18 +0,0 @@
name: Go
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.23' ]
steps:
- uses: actions/checkout@v4
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

View File

@@ -1,19 +0,0 @@
# Variables
REPO_REMOTE := origin
# Helper function to get the latest tag and increment the version
define next_version
latest_tag=$$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"); \
major=$$(echo $$latest_tag | sed -E 's/^v([0-9]+)\.[0-9]+\.[0-9]+/\1/'); \
minor=$$(echo $$latest_tag | sed -E 's/^v[0-9]+\.([0-9]+)\.[0-9]+/\1/'); \
patch=$$(echo $$latest_tag | sed -E 's/^v[0-9]+\.[0-9]+\.([0-9]+)/\1/'); \
next_patch=$$(($$patch + 1)); \
echo "v$$major.$$minor.$$next_patch"
endef
# Target to tag and push the next version
release:
@next_version=$$($(next_version)); \
echo "Tagging with version $$next_version";
git tag $$next_version; \
git push $(REPO_REMOTE) $$next_version

View File

@@ -7,7 +7,7 @@ A mix of useful packages, some are works in progress.
## Install
```
go get git.sdf.org/jchenry/x
go get github.com/jchenry/x
```
## Contributing

3
cache/doc.go vendored
View File

@@ -1,3 +0,0 @@
package cache
// Cache package cribbed from https://varunksaini.com/tiered-cache-in-go/ whom i have i high respect for as a former colleague.

6
cache/interface.go vendored
View File

@@ -1,6 +0,0 @@
package cache
type Interface[K comparable, V any] interface {
Get(key K) V
Put(key K, value V)
}

40
cache/tiered.go vendored
View File

@@ -1,40 +0,0 @@
package cache
import (
"reflect"
"git.sdf.org/jchenry/x"
)
type tieredCache[K comparable, V any] struct {
inner Interface[K, V]
outer Interface[K, V]
}
func NewTieredCache[K comparable, V any](inner, outer Interface[K, V]) Interface[K, V] {
x.Assert(inner != nil, "cache.NewTieredCache: inner cannot be nil")
x.Assert(outer != nil, "cache.NewTieredCache: outer cannot be nil")
return &tieredCache[K, V]{
inner: inner,
outer: outer,
}
}
func (t *tieredCache[K, V]) Get(key K) V {
var zero, value V
value = t.inner.Get(key)
if reflect.DeepEqual(value, zero) {
value = t.outer.Get(key)
// if required, add value to inner cache for future requests
}
return value
}
func (t *tieredCache[K, V]) Put(key K, value V) {
t.inner.Put(key, value)
// add key to outer cache asynchronously
go func(key K) {
t.outer.Put(key, value)
}(key)
}

View File

@@ -1,41 +0,0 @@
package sort
// Node is a generic interface representing a graph node.
type Node[T any] interface {
// Value returns the value of the node.
Value() T
// Adjacencies returns a slice of nodes adjacent to this node
Adjacencies() []Node[T]
}
// Topo performs a topological sort on a slice of nodes in place.
func TopoSort[T any](nodes []Node[T]) {
v := make(map[Node[T]]bool)
pos := 0
var dfs func(Node[T])
dfs = func(n Node[T]) {
v[n] = true
for _, e := range n.Adjacencies() {
if !v[e] {
dfs(e)
}
}
nodes[pos] = n
pos++
}
for _, n := range nodes {
if !v[n] {
dfs(n)
}
}
}
// RTopoSort performs a reverse topological sort on a slice of nodes in place.
func RTopoSort[T any](nodes []Node[T]) {
TopoSort(nodes)
for i, j := 0, len(nodes)-1; i < j; i, j = i+1, j-1 {
nodes[i], nodes[j] = nodes[j], nodes[i]
}
}

View File

@@ -3,8 +3,6 @@ package database
import (
"context"
"database/sql"
"git.sdf.org/jchenry/x"
)
type Func func(db *sql.DB)
@@ -15,7 +13,6 @@ type Actor struct {
}
func (a *Actor) Run(ctx context.Context) error {
x.Assert(ctx != nil, "Actor.Run: context cannot be nil")
for {
select {
case f := <-a.ActionChan:

View File

@@ -7,7 +7,7 @@ Example usage:
ctx, _ := context.WithCancel(context.Background())
dba = &db.Actor{
DB: s.db,
ActionChan: make(chan database.Func),
ActionChan: make(chan db.Func),
}
go dba.Run(ctx)

4
go.mod
View File

@@ -1,3 +1,3 @@
module git.sdf.org/jchenry/x
module github.com/jchenry/x
go 1.22
go 1.18

15
log/logger.go Normal file
View File

@@ -0,0 +1,15 @@
package log
// Logger is a logging interface with only the essentials that a function that needs to log should care about. Compatible with standard Go logger.
type Logger interface {
Print(v ...interface{})
Printf(format string, v ...interface{})
Println(v ...interface{})
}
// None provides a logger that doesnt log anything
type None struct{}
func (n None) Print(v ...interface{}) {}
func (n None) Printf(format string, v ...interface{}) {}
func (n None) Println(v ...interface{}) {}

View File

@@ -6,13 +6,9 @@ import (
"fmt"
"net/http"
"strings"
"git.sdf.org/jchenry/x"
)
func BasicAuth(h http.Handler, htpasswd map[string]string, realm string) http.HandlerFunc {
x.Assert(len(htpasswd) > 0, "http.BasicAuth: htpassword cannot be empty")
x.Assert(len(realm) > 0, "http.BasicAuth: realm cannot be empty")
rlm := fmt.Sprintf(`Basic realm="%s"`, realm)
sha1 := func(password string) string {
s := sha1.New()
@@ -24,7 +20,7 @@ func BasicAuth(h http.Handler, htpasswd map[string]string, realm string) http.Ha
user, pass, _ := r.BasicAuth()
if pw, ok := htpasswd[user]; !ok || !strings.EqualFold(pass, sha1(pw)) {
w.Header().Set("WWW-Authenticate", rlm)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
http.Error(w, "Unauthorized", 401)
return
}
h.ServeHTTP(w, r)

View File

@@ -30,7 +30,7 @@ func MutliHandler(h map[string]http.Handler) (http.HandlerFunc, error) {
if hdlr, ok := h[r.Method]; ok {
hdlr.ServeHTTP(w, r)
} else {
NotAllowedHandler.ServeHTTP(w, r)
NotImplementedHandler.ServeHTTP(w, r)
}
}, nil
}

95
net/http/mux.go Normal file
View File

@@ -0,0 +1,95 @@
package http
import (
"net/http"
"strconv"
"strings"
)
type ServeMux struct {
routes []route
}
func (mux *ServeMux) Handle(pattern string, handler http.Handler, pathParams ...any) {
mux.routes = append(mux.routes, newRoute(pattern, handler, pathParams...))
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), pathParams ...any) {
mux.routes = append(mux.routes, newRoute(pattern, http.HandlerFunc(handler), pathParams...))
}
func (mux *ServeMux) Handler(r *http.Request) (h http.Handler, pattern string) {
for _, rte := range mux.routes {
switch {
case rte.matcher(r):
return rte.handler, rte.pattern
}
}
return http.HandlerFunc(http.NotFound), ""
}
func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(http.StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
type route struct {
pattern string
matcher func(r *http.Request) bool
handler http.HandlerFunc
}
func newRoute(pattern string, handler http.Handler, vars ...interface{}) route {
return route{
pattern,
func(r *http.Request) bool {
return match(r.URL.Path, pattern, vars...)
},
handler.ServeHTTP,
}
}
// match reports whether path matches the given pattern, which is a
// path with '+' wildcards wherever you want to use a parameter. Path
// parameters are assigned to the pointers in vars (len(vars) must be
// the number of wildcards), which must be of type *string or *int.
func match(path, pattern string, vars ...interface{}) bool {
for ; pattern != "" && path != ""; pattern = pattern[1:] {
switch pattern[0] {
case '+':
// '+' matches till next slash in path
slash := strings.IndexByte(path, '/')
if slash < 0 {
slash = len(path)
}
segment := path[:slash]
path = path[slash:]
switch p := vars[0].(type) {
case *string:
*p = segment
case *int:
n, err := strconv.Atoi(segment)
if err != nil || n < 0 {
return false
}
*p = n
default:
panic("vars must be *string or *int")
}
vars = vars[1:]
case path[0]:
// non-'+' pattern byte must match path byte
path = path[1:]
default:
return false
}
}
return path == "" && pattern == ""
}

View File

@@ -15,8 +15,7 @@ func (s StatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
var (
NotFoundHandler = StatusHandler(http.StatusNotFound)
NotImplementedHandler = StatusHandler(http.StatusNotImplemented)
NotLegalHandler = StatusHandler(http.StatusUnavailableForLegalReasons)
NotAllowedHandler = StatusHandler(http.StatusMethodNotAllowed)
NotFoundHandler = StatusHandler(404)
NotImplementedHandler = StatusHandler(501)
NotLegalHandler = StatusHandler(451)
)

96
pkg.go
View File

@@ -1,96 +0,0 @@
package x
import (
"errors"
"fmt"
"runtime"
)
// Assert evalueates a condition and if it fails, panics.
func Assert(cond bool, msg string) {
if !cond {
panic(errors.New(msg))
}
}
// Check evaluates a condition, adds an error to its list and continues
func Check(cond bool, err error) I {
return new(invariants).Check(cond, err)
}
// func ExampleCheck() {
// }
type I interface {
Check(cond bool, err error) I
Join() error
First() error
All() []error
}
type invariants struct {
errs []error
}
// Check evaluates a condition, adds an error to its list and continues
func (i *invariants) Check(cond bool, err error) I {
if !cond {
i.errs = append(i.errs, err)
}
return i
}
// Join returns all an error wrapping all errors that have been seen by the checks
func (i *invariants) Join() error {
return errors.Join(i.errs...)
}
// First returns the first error found by the checks
func (i *invariants) First() error {
if len(i.errs) > 0 {
return i.errs[0]
}
return nil
}
// All returns all errors found by the checks as a list of errors
func (i *invariants) All() []error {
return i.errs
}
type xError struct {
LineNo int
File string
E error
Debug bool
}
func (e *xError) Error() string {
if e.Debug {
return fmt.Sprintf(
"%s\n\t%s:%d", e.E, e.File, e.LineNo,
)
}
return e.E.Error()
}
func (e *xError) Unwrap() error {
return e.E
}
// NewError return an error that wrapped an error and optionally provides the file and line number the error occured on
func NewError(unwrapped error, debug bool) error {
_, file, line, ok := runtime.Caller(1)
if !ok {
file = "unknown"
line = 0
}
return &xError{
LineNo: line,
File: file,
E: unwrapped,
Debug: debug,
}
}

95
rest/collection.go Executable file
View File

@@ -0,0 +1,95 @@
package rest
import (
"net/http"
"net/url"
"path/filepath"
"sync"
"github.com/jchenry/x/encoding"
"github.com/jchenry/x/log"
)
// Example: Resource(p, c, JSONEncoder, json.Decode(func()interface{}{return &foo{}}), log.None{})
func Resource(p *sync.Pool, g Gateway, e EntityEncoder, d encoding.Decoder, l log.Logger) http.HandlerFunc {
return restVerbHandler(
GetResource(g, e, l),
PostResource(g, d, p, l),
PutResource(g, e, d, p, l),
DeleteResource(g, l),
)
}
func GetResource(store Readable, encode EntityEncoder, log log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // GET
if id := filepath.Base(r.URL.Path); id != "" {
if e, err := store.Read(id); err == nil { // handle individual entity
encode(w, e)
} else {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error: %s", err)
}
} else {
if params, err := url.ParseQuery(r.URL.RawQuery); err == nil {
if e, err := store.All(params); err == nil { // handle all entities
encode(w, e)
} else {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error: %s", err)
}
} else {
w.WriteHeader(http.StatusBadRequest)
}
}
}
}
func PostResource(store Creatable, decode encoding.Decoder, pool *sync.Pool, log log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // POST TODO
e := pool.Get()
defer pool.Put(e)
if err := decode(r.Body, e); err == nil {
if err = store.Create(e); err == nil {
w.WriteHeader(http.StatusCreated)
} else {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error: %s", err)
}
} else {
w.WriteHeader(http.StatusBadRequest)
}
}
}
func PutResource(store Updatable, encode EntityEncoder, decode encoding.Decoder, pool *sync.Pool, log log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // PUT TODO
e := pool.Get()
defer pool.Put(e)
if err := decode(r.Body, e); err == nil {
if err = store.Update(e); err == nil {
w.WriteHeader(http.StatusAccepted)
encode(w, e)
} else {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error: %s", err)
}
} else {
w.WriteHeader(http.StatusBadRequest)
}
}
}
func DeleteResource(store Deletable, log log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // DELETE TODO
if id := filepath.Base(r.URL.Path); id != "" {
if err := store.Delete(id); err == nil {
w.WriteHeader(http.StatusNoContent)
} else {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error: %s", err)
}
} else {
w.WriteHeader(http.StatusBadRequest)
}
}
}

23
rest/entity_handler.go Normal file
View File

@@ -0,0 +1,23 @@
package rest
import (
gohttp "net/http"
"github.com/jchenry/x/net/http"
)
// EntityHandler returns a handler that provides restful verbs, following a CRUD model
func restVerbHandler(
get gohttp.Handler,
post gohttp.Handler,
put gohttp.Handler,
delete gohttp.Handler,
) gohttp.HandlerFunc {
h, _ := http.MutliHandler(map[string]gohttp.Handler{
gohttp.MethodGet: get,
gohttp.MethodPost: post,
gohttp.MethodPut: put,
gohttp.MethodDelete: delete,
})
return h
}

25
rest/gateway.go Normal file
View File

@@ -0,0 +1,25 @@
package rest
type Creatable interface {
Create(e interface{}) error
}
type Updatable interface {
Update(e interface{}) error
}
type Deletable interface {
Delete(id string) error
}
type Readable interface {
All(filters map[string][]string) (interface{}, error)
Read(id string) (interface{}, error)
}
type Gateway interface {
Creatable
Updatable
Deletable
Readable
}

32
rest/response_encoder.go Normal file
View File

@@ -0,0 +1,32 @@
package rest
import (
"net/http"
"github.com/jchenry/x/encoding"
"github.com/jchenry/x/encoding/json"
"github.com/jchenry/x/encoding/xml"
)
type EntityEncoder func(w http.ResponseWriter, e interface{})
func JSONEncoder(w http.ResponseWriter, e interface{}) error {
return EntityResponseEncoder(w, "application/json", json.Encoder, e)
}
func XMLEncoder(w http.ResponseWriter, e interface{}) error {
return EntityResponseEncoder(w, "application/xml", xml.Encoder, e)
}
func EntityResponseEncoder(w http.ResponseWriter, contentType string, encoder encoding.Encoder, e interface{}) error {
w.Header().Set("content-type", contentType)
return encoder(w, e)
}
func ErrorResponseEncoder(w http.ResponseWriter, contentType string, encoder encoding.Encoder, status int, err error) error {
w.WriteHeader(status)
return EntityResponseEncoder(w, contentType, encoder, map[string]interface{}{
"status": status,
"message": err.Error,
})
}

View File

@@ -1,6 +1,7 @@
package snowflake
import (
"fmt"
"hash/fnv"
"math"
"net"
@@ -14,48 +15,42 @@ const (
nodeIDBits = 10
sequenceBits = 12
// Custom Epoch (January 1, 2015 Midnight UTC = 2015-01-01T00:00:00Z) .
// Custom Epoch (January 1, 2015 Midnight UTC = 2015-01-01T00:00:00Z)
customEpoch int64 = 1420070400000
)
var (
maxNodeID int64
maxSequence int64
timestampMutex sync.Mutex
sequenceMutex sync.Mutex
nodeID int64
lastTimestamp int64 = 0
sequence int64
)
var maxNodeID int64
var maxSequence int64
const two = 2
var nodeID int64
var lastTimestamp int64 = 0
var sequence int64
func init() {
maxNodeID = int64(math.Pow(two, nodeIDBits) - 1)
maxSequence = int64(math.Pow(two, sequenceBits) - 1)
maxNodeID = int64(math.Pow(2, nodeIDBits) - 1)
maxSequence = int64(math.Pow(2, sequenceBits) - 1)
nodeID = generateNodeID()
}
func generateNodeID() int64 {
var nodeID int64
if interfaces, err := net.Interfaces(); err == nil {
h := fnv.New32a()
for _, i := range interfaces {
h.Write(i.HardwareAddr)
}
nodeID = int64(h.Sum32())
} else {
panic("interfaces not available")
}
nodeID = nodeID & maxNodeID
return nodeID
}
// Next returns the next logical snowflake.
var timestampMutex sync.Mutex
var sequenceMutex sync.Mutex
// Next returns the next logical snowflake
func Next() int64 {
timestampMutex.Lock()
currentTimestamp := ts()
@@ -81,6 +76,7 @@ func Next() int64 {
id |= (nodeID << sequenceBits)
id |= sequence
fmt.Printf("%b\n", id)
return id
}
@@ -92,6 +88,5 @@ func waitNextMillis(currentTimestamp int64) int64 {
for currentTimestamp == lastTimestamp {
currentTimestamp = ts()
}
return currentTimestamp
}

View File

@@ -9,6 +9,7 @@ func TestNext(t *testing.T) {
fmt.Printf("node id: %b\n", generateNodeID())
fmt.Printf("timestamp: %b\n", ts())
fmt.Printf("full token: %b\n", Next())
// t.Fail()
}
func BenchmarkNext(b *testing.B) {

View File

@@ -6,7 +6,7 @@ import (
"testing"
"time"
"git.sdf.org/jchenry/x/time/arvelie"
"github.com/jchenry/x/time/arvelie"
)
func TestFromDate(t *testing.T) {

View File

@@ -1,5 +0,0 @@
package time
import "time"
const ISO8601 = time.RFC3339

View File

@@ -6,7 +6,7 @@ import (
"testing"
"time"
"git.sdf.org/jchenry/x/time/neralie"
"github.com/jchenry/x/time/neralie"
)
func TestFromTime(t *testing.T) {