Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b7969f89b5 | ||
|
7e1daaf7d5 | ||
|
40bc496b20 | ||
|
401f5c3848 | ||
|
f0fd3f8df1 | ||
|
8de3b04032 | ||
|
f44e43174c | ||
|
fe49018479 | ||
|
94bc398ea7 | ||
|
2973912fde | ||
|
b738f78e6e | ||
|
74f9fc64f4 | ||
|
612a5c8387 | ||
|
a1e52a7399 | ||
|
79e3d1f0b9 | ||
|
8b77b5acc8 | ||
|
4665f2ccf9 | ||
|
7154029136 | ||
|
9f88dc9530 | ||
|
1ec6f3c5c3 | ||
|
390a54eb96 | ||
|
d312b58fdf | ||
|
b19ba53491 | ||
|
859849bd47 | ||
|
7cf53a2f2d | ||
|
593c8db66e | ||
|
74c99d013c | ||
|
618cdcc095 | ||
|
32337f4bc8 | ||
|
888e1b9c7d |
18
.gitea/workflows/build.yaml
Normal file
18
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
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 ./...
|
19
Makefile
Normal file
19
Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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
|
@@ -7,7 +7,7 @@ A mix of useful packages, some are works in progress.
|
||||
## Install
|
||||
|
||||
```
|
||||
go get github.com/jchenry/x
|
||||
go get git.sdf.org/jchenry/x
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
3
cache/doc.go
vendored
Normal file
3
cache/doc.go
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
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
Normal file
6
cache/interface.go
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package cache
|
||||
|
||||
type Interface[K comparable, V any] interface {
|
||||
Get(key K) V
|
||||
Put(key K, value V)
|
||||
}
|
40
cache/tiered.go
vendored
Normal file
40
cache/tiered.go
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
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)
|
||||
}
|
41
container/graph/topo.go
Normal file
41
container/graph/topo.go
Normal file
@@ -0,0 +1,41 @@
|
||||
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]
|
||||
}
|
||||
}
|
@@ -3,6 +3,8 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.sdf.org/jchenry/x"
|
||||
)
|
||||
|
||||
type Func func(db *sql.DB)
|
||||
@@ -13,6 +15,7 @@ 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:
|
||||
|
@@ -7,7 +7,7 @@ Example usage:
|
||||
ctx, _ := context.WithCancel(context.Background())
|
||||
dba = &db.Actor{
|
||||
DB: s.db,
|
||||
ActionChan: make(chan db.Func),
|
||||
ActionChan: make(chan database.Func),
|
||||
}
|
||||
|
||||
go dba.Run(ctx)
|
||||
|
4
go.mod
4
go.mod
@@ -1,3 +1,3 @@
|
||||
module github.com/jchenry/x
|
||||
module git.sdf.org/jchenry/x
|
||||
|
||||
go 1.18
|
||||
go 1.22
|
||||
|
@@ -1,14 +0,0 @@
|
||||
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 {
|
||||
Fatal(v ...any)
|
||||
Fatalf(format string, v ...any)
|
||||
Fatalln(v ...any)
|
||||
Panic(v ...any)
|
||||
Panicf(format string, v ...any)
|
||||
Panicln(v ...any)
|
||||
Print(v ...any)
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
}
|
14
log/none.go
14
log/none.go
@@ -1,14 +0,0 @@
|
||||
package log
|
||||
|
||||
// None provides a logger that doesnt log anything
|
||||
type None struct{}
|
||||
|
||||
func (n None) Fatal(v ...any) {}
|
||||
func (n None) Fatalf(format string, v ...any) {}
|
||||
func (n None) Fatalln(v ...any) {}
|
||||
func (n None) Panic(v ...any) {}
|
||||
func (n None) Panicf(format string, v ...any) {}
|
||||
func (n None) Panicln(v ...any) {}
|
||||
func (n None) Print(v ...any) {}
|
||||
func (n None) Printf(format string, v ...any) {}
|
||||
func (n None) Println(v ...any) {}
|
@@ -6,9 +6,13 @@ 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()
|
||||
@@ -20,7 +24,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", 401)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
|
106
net/http/mux.go
106
net/http/mux.go
@@ -1,106 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// wayContextKey is the context key type for storing
|
||||
// parameters in context.Context.
|
||||
type wayContextKey string
|
||||
|
||||
// Router routes HTTP requests.
|
||||
type ServeMux struct {
|
||||
routes []*route
|
||||
// NotFound is the http.Handler to call when no routes
|
||||
// match. By default uses http.NotFoundHandler().
|
||||
NotFound http.Handler
|
||||
}
|
||||
|
||||
// NewRouter makes a new Router.
|
||||
func NewServeMux() *ServeMux {
|
||||
return &ServeMux{
|
||||
NotFound: http.NotFoundHandler(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ServeMux) pathSegments(p string) []string {
|
||||
return strings.Split(strings.Trim(p, "/"), "/")
|
||||
}
|
||||
|
||||
// Handle adds a handler with the specified pattern.
|
||||
// Pattern can contain path segments such as: /item/:id which is
|
||||
// accessible via the Param function.
|
||||
// If pattern ends with trailing /, it acts as a prefix.
|
||||
func (r *ServeMux) Handle(pattern string, handler http.Handler) {
|
||||
route := &route{
|
||||
segs: r.pathSegments(pattern),
|
||||
handler: handler,
|
||||
prefix: strings.HasSuffix(pattern, "/") || strings.HasSuffix(pattern, "..."),
|
||||
}
|
||||
r.routes = append(r.routes, route)
|
||||
}
|
||||
|
||||
// HandleFunc is the http.HandlerFunc alternative to http.Handle.
|
||||
func (r *ServeMux) HandleFunc(pattern string, fn http.HandlerFunc) {
|
||||
r.Handle(pattern, fn)
|
||||
}
|
||||
|
||||
// ServeHTTP routes the incoming http.Request based on path
|
||||
func (r *ServeMux) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
segs := r.pathSegments(req.URL.Path)
|
||||
for _, route := range r.routes {
|
||||
if ctx, ok := route.match(req.Context(), r, segs); ok {
|
||||
route.handler.ServeHTTP(w, req.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
}
|
||||
r.NotFound.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// Param gets the path parameter from the specified Context.
|
||||
// Returns an empty string if the parameter was not found.
|
||||
func Param(ctx context.Context, param string) string {
|
||||
vStr, ok := ctx.Value(wayContextKey(param)).(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return vStr
|
||||
}
|
||||
|
||||
type route struct {
|
||||
segs []string
|
||||
handler http.Handler
|
||||
prefix bool
|
||||
}
|
||||
|
||||
func (r *route) match(ctx context.Context, router *ServeMux, segs []string) (context.Context, bool) {
|
||||
if len(segs) > len(r.segs) && !r.prefix {
|
||||
return nil, false
|
||||
}
|
||||
for i, seg := range r.segs {
|
||||
if i > len(segs)-1 {
|
||||
return nil, false
|
||||
}
|
||||
isParam := false
|
||||
if strings.HasPrefix(seg, "{") {
|
||||
isParam = true
|
||||
seg = strings.Trim(seg, "{}")
|
||||
}
|
||||
if !isParam { // verbatim check
|
||||
if strings.HasSuffix(seg, "...") {
|
||||
if strings.HasPrefix(segs[i], seg[:len(seg)-3]) {
|
||||
return ctx, true
|
||||
}
|
||||
}
|
||||
if seg != segs[i] {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if isParam {
|
||||
ctx = context.WithValue(ctx, wayContextKey(seg), segs[i])
|
||||
}
|
||||
}
|
||||
return ctx, true
|
||||
}
|
@@ -1,231 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var tests = []struct {
|
||||
// RouteMethod string
|
||||
RoutePattern string
|
||||
|
||||
Method string
|
||||
Path string
|
||||
Match bool
|
||||
Params map[string]string
|
||||
}{
|
||||
// simple path matching
|
||||
{
|
||||
"/one",
|
||||
"GET", "/one", true, nil,
|
||||
},
|
||||
{
|
||||
"/two",
|
||||
"GET", "/two", true, nil,
|
||||
},
|
||||
{
|
||||
"/three",
|
||||
"GET", "/three", true, nil,
|
||||
},
|
||||
// methods
|
||||
{
|
||||
"/methodcase",
|
||||
"GET", "/methodcase", true, nil,
|
||||
},
|
||||
{
|
||||
"/methodcase",
|
||||
"get", "/methodcase", true, nil,
|
||||
},
|
||||
{
|
||||
"/methodcase",
|
||||
"get", "/methodcase", true, nil,
|
||||
},
|
||||
{
|
||||
"/method1",
|
||||
"POST", "/method1", true, nil,
|
||||
},
|
||||
{
|
||||
"/method2",
|
||||
"GET", "/method2", true, nil,
|
||||
},
|
||||
{
|
||||
"/method3",
|
||||
"PUT", "/method3", true, nil,
|
||||
},
|
||||
// all methods
|
||||
{
|
||||
"/all-methods",
|
||||
"GET", "/all-methods", true, nil,
|
||||
},
|
||||
{
|
||||
"/all-methods",
|
||||
"POST", "/all-methods", true, nil,
|
||||
},
|
||||
{
|
||||
"/all-methods",
|
||||
"PUT", "/all-methods", true, nil,
|
||||
},
|
||||
// nested
|
||||
{
|
||||
"/parent/child/one",
|
||||
"GET", "/parent/child/one", true, nil,
|
||||
},
|
||||
{
|
||||
"/parent/child/two",
|
||||
"GET", "/parent/child/two", true, nil,
|
||||
},
|
||||
{
|
||||
"/parent/child/three",
|
||||
"GET", "/parent/child/three", true, nil,
|
||||
},
|
||||
// slashes
|
||||
{
|
||||
"slashes/one",
|
||||
"GET", "/slashes/one", true, nil,
|
||||
},
|
||||
{
|
||||
"/slashes/two",
|
||||
"GET", "slashes/two", true, nil,
|
||||
},
|
||||
{
|
||||
"slashes/three/",
|
||||
"GET", "/slashes/three", true, nil,
|
||||
},
|
||||
{
|
||||
"/slashes/four",
|
||||
"GET", "slashes/four/", true, nil,
|
||||
},
|
||||
// prefix
|
||||
{
|
||||
"/prefix/",
|
||||
"GET", "/prefix/anything/else", true, nil,
|
||||
},
|
||||
{
|
||||
"/not-prefix",
|
||||
"GET", "/not-prefix/anything/else", false, nil,
|
||||
},
|
||||
{
|
||||
"/prefixdots...",
|
||||
"GET", "/prefixdots/anything/else", true, nil,
|
||||
},
|
||||
{
|
||||
"/prefixdots...",
|
||||
"GET", "/prefixdots", true, nil,
|
||||
},
|
||||
// path params
|
||||
{
|
||||
"/path-param/{id}",
|
||||
"GET", "/path-param/123", true, map[string]string{"id": "123"},
|
||||
},
|
||||
{
|
||||
"/path-params/{era}/{group}/{member}",
|
||||
"GET", "/path-params/60s/beatles/lennon", true, map[string]string{
|
||||
"era": "60s",
|
||||
"group": "beatles",
|
||||
"member": "lennon",
|
||||
},
|
||||
},
|
||||
{
|
||||
"/path-params-prefix/{era}/{group}/{member}/",
|
||||
"GET", "/path-params-prefix/60s/beatles/lennon/yoko", true, map[string]string{
|
||||
"era": "60s",
|
||||
"group": "beatles",
|
||||
"member": "lennon",
|
||||
},
|
||||
},
|
||||
// misc no matches
|
||||
{
|
||||
"/not/enough",
|
||||
"GET", "/not/enough/items", false, nil,
|
||||
},
|
||||
{
|
||||
"/not/enough/items",
|
||||
"GET", "/not/enough", false, nil,
|
||||
},
|
||||
}
|
||||
|
||||
func TestWay(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
r := NewServeMux()
|
||||
match := false
|
||||
var ctx context.Context
|
||||
r.Handle(test.RoutePattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
match = true
|
||||
ctx = r.Context()
|
||||
}))
|
||||
req, err := http.NewRequest(test.Method, test.Path, nil)
|
||||
if err != nil {
|
||||
t.Errorf("NewRequest: %s", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if match != test.Match {
|
||||
t.Errorf("expected match %v but was %v: %s %s", test.Match, match, test.Method, test.Path)
|
||||
}
|
||||
if len(test.Params) > 0 {
|
||||
for expK, expV := range test.Params {
|
||||
// check using helper
|
||||
actualValStr := Param(ctx, expK)
|
||||
if actualValStr != expV {
|
||||
t.Errorf("Param: context value %s expected \"%s\" but was \"%s\"", expK, expV, actualValStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleRoutesDifferentMethods(t *testing.T) {
|
||||
r := NewServeMux()
|
||||
var match string
|
||||
|
||||
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
match = "GET /route"
|
||||
case http.MethodDelete:
|
||||
match = "DELETE /route"
|
||||
case http.MethodPost:
|
||||
match = "POST /route"
|
||||
}
|
||||
}))
|
||||
|
||||
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
match = "GET /route"
|
||||
}))
|
||||
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
match = "DELETE /route"
|
||||
}))
|
||||
r.Handle("/route", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
match = "POST /route"
|
||||
}))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/route", nil)
|
||||
if err != nil {
|
||||
t.Errorf("NewRequest: %s", err)
|
||||
}
|
||||
r.ServeHTTP(httptest.NewRecorder(), req)
|
||||
if match != "GET /route" {
|
||||
t.Errorf("unexpected: %s", match)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodDelete, "/route", nil)
|
||||
if err != nil {
|
||||
t.Errorf("NewRequest: %s", err)
|
||||
}
|
||||
r.ServeHTTP(httptest.NewRecorder(), req)
|
||||
if match != "DELETE /route" {
|
||||
t.Errorf("unexpected: %s", match)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, "/route", nil)
|
||||
if err != nil {
|
||||
t.Errorf("NewRequest: %s", err)
|
||||
}
|
||||
r.ServeHTTP(httptest.NewRecorder(), req)
|
||||
if match != "POST /route" {
|
||||
t.Errorf("unexpected: %s", match)
|
||||
}
|
||||
|
||||
}
|
96
pkg.go
Normal file
96
pkg.go
Normal file
@@ -0,0 +1,96 @@
|
||||
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,
|
||||
}
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
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
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
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
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
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,
|
||||
})
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
package snowflake
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math"
|
||||
"net"
|
||||
@@ -15,42 +14,48 @@ 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
|
||||
var maxSequence int64
|
||||
var (
|
||||
maxNodeID int64
|
||||
maxSequence int64
|
||||
timestampMutex sync.Mutex
|
||||
sequenceMutex sync.Mutex
|
||||
nodeID int64
|
||||
lastTimestamp int64 = 0
|
||||
sequence int64
|
||||
)
|
||||
|
||||
var nodeID int64
|
||||
var lastTimestamp int64 = 0
|
||||
var sequence int64
|
||||
const two = 2
|
||||
|
||||
func init() {
|
||||
maxNodeID = int64(math.Pow(2, nodeIDBits) - 1)
|
||||
maxSequence = int64(math.Pow(2, sequenceBits) - 1)
|
||||
maxNodeID = int64(math.Pow(two, nodeIDBits) - 1)
|
||||
maxSequence = int64(math.Pow(two, 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
|
||||
}
|
||||
|
||||
var timestampMutex sync.Mutex
|
||||
var sequenceMutex sync.Mutex
|
||||
|
||||
// Next returns the next logical snowflake
|
||||
// Next returns the next logical snowflake.
|
||||
func Next() int64 {
|
||||
timestampMutex.Lock()
|
||||
currentTimestamp := ts()
|
||||
@@ -76,7 +81,6 @@ func Next() int64 {
|
||||
id |= (nodeID << sequenceBits)
|
||||
id |= sequence
|
||||
|
||||
fmt.Printf("%b\n", id)
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -88,5 +92,6 @@ func waitNextMillis(currentTimestamp int64) int64 {
|
||||
for currentTimestamp == lastTimestamp {
|
||||
currentTimestamp = ts()
|
||||
}
|
||||
|
||||
return currentTimestamp
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ 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) {
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jchenry/x/time/arvelie"
|
||||
"git.sdf.org/jchenry/x/time/arvelie"
|
||||
)
|
||||
|
||||
func TestFromDate(t *testing.T) {
|
||||
|
5
time/format.go
Normal file
5
time/format.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package time
|
||||
|
||||
import "time"
|
||||
|
||||
const ISO8601 = time.RFC3339
|
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jchenry/x/time/neralie"
|
||||
"git.sdf.org/jchenry/x/time/neralie"
|
||||
)
|
||||
|
||||
func TestFromTime(t *testing.T) {
|
||||
|
Reference in New Issue
Block a user