36 Commits

Author SHA1 Message Date
Colin Henry
3d71b65428 added trie
Some checks failed
Go / build (1.23) (push) Failing after 2m42s
2025-07-05 11:56:37 -07:00
Colin Henry
4cf0b874d6 fixed tio arvelie 2025-07-05 11:56:37 -07:00
Colin Henry
475e091d21 remove drone.yml
All checks were successful
Go / build (1.23) (push) Successful in 2m53s
2025-06-20 00:07:25 -07:00
Colin Henry
56752521c2 added doc
All checks were successful
Go / build (1.23) (push) Successful in 2m31s
2025-03-18 22:28:20 -07:00
Colin Henry
6aa7788fd0 added transactional wrapper, and support for it 2025-03-10 22:59:03 -07:00
Colin Henry
f88f00e78c fixed release target
All checks were successful
Go / build (1.23) (push) Successful in 17s
2024-10-27 12:30:45 -07:00
Colin Henry
b7969f89b5 fixed release target
All checks were successful
Go / build (1.23) (push) Successful in 17s
2024-10-27 12:28:57 -07:00
Colin Henry
7e1daaf7d5 added makefile to do releases
All checks were successful
Go / build (1.23) (push) Successful in 28s
2024-10-27 12:04:01 -07:00
Colin Henry
40bc496b20 added makefile to do releases 2024-10-27 12:03:34 -07:00
Colin Henry
401f5c3848 WIP: added container/graph
All checks were successful
Go / build (1.23) (push) Successful in 2m8s
2024-10-27 11:58:47 -07:00
Colin Henry
f0fd3f8df1 added come comments
Some checks failed
Go / build (1.23) (push) Has been cancelled
2024-10-27 11:58:07 -07:00
Colin Henry
8de3b04032 fix(ci): updated support matrix
All checks were successful
Go / build (1.23) (push) Successful in 3m3s
2024-09-29 12:02:35 -07:00
Colin Henry
f44e43174c added wrapper error that will provide file/line origin of an error
Some checks failed
Go / build (1.19) (push) Failing after 13s
2024-09-29 11:59:01 -07:00
Colin Henry
fe49018479 added new check capability and added assets to a few other packages
Some checks failed
Go / build (1.19) (push) Failing after 22s
2024-09-28 15:19:00 -07:00
Colin Henry
94bc398ea7 added assert function
Some checks failed
Go / build (1.19) (push) Failing after 4m5s
2024-09-06 20:29:29 -07:00
Colin Henry
2973912fde updated minimum go version 2024-09-06 20:28:23 -07:00
Colin Henry
b738f78e6e reomved now unnecessary libraries 2024-09-06 20:27:26 -07:00
Colin Henry
74f9fc64f4 removed deprecated log package 2024-09-05 23:36:49 -07:00
Colin Henry
612a5c8387 removing deprecated rest package 2024-09-05 22:32:04 -07:00
Colin Henry
a1e52a7399 changing module name 2024-09-05 22:30:30 -07:00
Colin Henry
79e3d1f0b9 fixed busted test step
All checks were successful
Go / build (1.19) (push) Successful in 46s
2024-04-27 15:43:57 -07:00
Colin Henry
8b77b5acc8 added gitea action
Some checks failed
Go / build (1.19) (push) Failing after 3m29s
2024-04-26 23:20:51 -07:00
Colin Henry
4665f2ccf9 deprecating packages 2023-08-11 21:59:16 -07:00
Colin Henry
7154029136 linted 2023-04-05 21:20:45 -07:00
Colin Henry
9f88dc9530 added ISO8601 constant 2023-04-04 23:19:22 -07:00
Colin Henry
1ec6f3c5c3 updated doc 2023-04-04 23:19:05 -07:00
Colin Henry
390a54eb96 updated log interface name 2023-04-04 23:17:39 -07:00
Colin Henry
d312b58fdf updated go.mod for compatability with generics, as used by the cache 2023-01-14 00:12:30 -08:00
Colin Henry
b19ba53491 added appropriate attribution
for tierd cache stub
2023-01-14 00:09:00 -08:00
Colin Henry
859849bd47 stub of tiered cache 2022-12-15 22:00:51 -08:00
Colin Henry
7cf53a2f2d sped up mappedParam by removing joins and splits 2022-09-15 21:28:19 -07:00
Colin Henry
593c8db66e fixed unit tests 2022-09-15 21:21:51 -07:00
Colin Henry
74c99d013c well, maybe a little 2022-09-14 15:43:05 -07:00
Colin Henry
618cdcc095 no mo way 2022-09-14 15:41:43 -07:00
Colin Henry
32337f4bc8 fixed non-standard return code 2022-09-14 15:41:15 -07:00
Colin Henry
888e1b9c7d new functions to extract certain types of parameters 2022-09-14 15:39:41 -07:00
30 changed files with 573 additions and 579 deletions

View File

@@ -1,10 +0,0 @@
kind: pipeline
type: docker
name: default
steps:
- name: test
image: golang
commands:
- go test ./...
- go build ./...

View 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
View 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

View File

@@ -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
View 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
View 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
View 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
View 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]
}
}

101
container/trie/trie.go Normal file
View File

@@ -0,0 +1,101 @@
package trie
import (
"sort"
)
// Node represents a node in the trie
type Node struct {
children map[rune]*Node
isEnd bool
}
// New creates a new empty trie root node
func New() *Node {
return &Node{
children: make(map[rune]*Node),
isEnd: false,
}
}
// Add adds a word to the trie
func (n *Node) Add(word string) {
current := n
for _, char := range word {
if _, exists := current.children[char]; !exists {
current.children[char] = &Node{
children: make(map[rune]*Node),
isEnd: false,
}
}
current = current.children[char]
}
current.isEnd = true
}
// countWords recursively counts the number of words in the subtree
func (n *Node) countWords() int {
count := 0
if n.isEnd {
count++
}
for _, child := range n.children {
count += child.countWords()
}
return count
}
// Count returns the number of words that have the given prefix
func (n *Node) Count(prefix string) int {
current := n
for _, char := range prefix {
if _, exists := current.children[char]; !exists {
return 0
}
current = current.children[char]
}
return current.countWords()
}
// PrefixCount represents a prefix and its count
type PrefixCount struct {
Prefix string
Count int
}
// collectPrefixes recursively collects all prefixes and their counts
func collectPrefixes(n *Node, currentPrefix string, results *[]PrefixCount) {
count := n.countWords()
if count > 0 {
*results = append(*results, PrefixCount{
Prefix: currentPrefix,
Count: count,
})
}
// Sort children by rune to ensure consistent traversal order
chars := make([]rune, 0, len(n.children))
for char := range n.children {
chars = append(chars, char)
}
sort.Slice(chars, func(i, j int) bool {
return chars[i] < chars[j]
})
for _, char := range chars {
collectPrefixes(n.children[char], currentPrefix+string(char), results)
}
}
// TopPrefixes returns a sorted list of prefixes by their counts
func TopPrefixes(n *Node) []PrefixCount {
var results []PrefixCount
collectPrefixes(n, "", &results)
sort.Slice(results, func(i, j int) bool {
if results[i].Count == results[j].Count {
return results[i].Prefix < results[j].Prefix
}
return results[i].Count > results[j].Count
})
return results
}

163
container/trie/trie_test.go Normal file
View File

@@ -0,0 +1,163 @@
package trie
import (
"testing"
)
func TestNew(t *testing.T) {
root := New()
if root == nil {
t.Error("New() returned nil")
}
if root.children == nil {
t.Error("New() root node has nil children map")
}
if root.isEnd {
t.Error("New() root node should not be marked as end")
}
}
func TestAddAndCount(t *testing.T) {
tests := []struct {
name string
words []string
prefix string
expected int
}{
{
name: "empty trie",
words: []string{},
prefix: "test",
expected: 0,
},
{
name: "single word",
words: []string{"hello"},
prefix: "hel",
expected: 1,
},
{
name: "multiple words same prefix",
words: []string{"hello", "help", "hell"},
prefix: "hel",
expected: 3,
},
{
name: "prefix not found",
words: []string{"hello", "help", "hell"},
prefix: "xyz",
expected: 0,
},
{
name: "empty prefix",
words: []string{"hello", "help", "hell"},
prefix: "",
expected: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := New()
for _, word := range tt.words {
root.Add(word)
}
if got := root.Count(tt.prefix); got != tt.expected {
t.Errorf("Count(%q) = %d, want %d", tt.prefix, got, tt.expected)
}
})
}
}
func TestTopPrefixes(t *testing.T) {
tests := []struct {
name string
words []string
expected []PrefixCount
}{
{
name: "empty trie",
words: []string{},
expected: []PrefixCount{},
},
{
name: "single word",
words: []string{"hello"},
expected: []PrefixCount{
{Prefix: "", Count: 1},
{Prefix: "h", Count: 1},
{Prefix: "he", Count: 1},
{Prefix: "hel", Count: 1},
{Prefix: "hell", Count: 1},
{Prefix: "hello", Count: 1},
},
},
{
name: "multiple words",
words: []string{"hello", "help", "hell", "helicopter"},
expected: []PrefixCount{
{Prefix: "", Count: 4},
{Prefix: "h", Count: 4},
{Prefix: "he", Count: 4},
{Prefix: "hel", Count: 4},
{Prefix: "hell", Count: 2},
{Prefix: "hello", Count: 1},
{Prefix: "help", Count: 1},
{Prefix: "heli", Count: 1},
{Prefix: "helic", Count: 1},
{Prefix: "helico", Count: 1},
{Prefix: "helicop", Count: 1},
{Prefix: "helicopt", Count: 1},
{Prefix: "helicopte", Count: 1},
{Prefix: "helicopter", Count: 1},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := New()
for _, word := range tt.words {
root.Add(word)
}
got := TopPrefixes(root)
if len(got) != len(tt.expected) {
t.Errorf("TopPrefixes() returned %d results, want %d", len(got), len(tt.expected))
return
}
for i, want := range tt.expected {
if i >= len(got) {
break
}
if got[i].Prefix != want.Prefix || got[i].Count != want.Count {
t.Errorf("TopPrefixes()[%d] = {Prefix: %q, Count: %d}, want {Prefix: %q, Count: %d}",
i, got[i].Prefix, got[i].Count, want.Prefix, want.Count)
}
}
})
}
}
func TestTopPrefixesSorting(t *testing.T) {
root := New()
words := []string{"a", "ab", "abc", "abcd", "abcde"}
for _, word := range words {
root.Add(word)
}
prefixes := TopPrefixes(root)
// Verify sorting by count (descending)
for i := 1; i < len(prefixes); i++ {
if prefixes[i].Count > prefixes[i-1].Count {
t.Errorf("Prefixes not sorted by count: %v", prefixes)
}
}
// Verify alphabetical sorting for equal counts
for i := 1; i < len(prefixes); i++ {
if prefixes[i].Count == prefixes[i-1].Count && prefixes[i].Prefix < prefixes[i-1].Prefix {
t.Errorf("Prefixes with equal counts not sorted alphabetically: %v", prefixes)
}
}
}

View File

@@ -3,9 +3,11 @@ package database
import (
"context"
"database/sql"
"git.sdf.org/jchenry/x"
)
type Func func(db *sql.DB)
type Func func(ctx context.Context, db *sql.DB) error
type Actor struct {
DB *sql.DB
@@ -13,10 +15,13 @@ 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:
f(a.DB)
if err:= f(ctx, a.DB); err != nil{
return err
}
case <-ctx.Done():
return ctx.Err()
}

View File

@@ -7,9 +7,21 @@ 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)
*/
/*
Transactor Example:
func insert(ctx context.Context, db *sql.DB) error{
// SQL HERE
}
...
dba.ActionChan <- db.WithTransaction(insert)
*/

29
database/transactor.go Normal file
View File

@@ -0,0 +1,29 @@
package database
import (
"context"
"database/sql"
"fmt"
)
// WithinTransaction is a functional equivalent of the Transactor interface created by Thibaut Rousseau's
// https://blog.thibaut-rousseau.com/blog/sql-transactions-in-go-the-good-way/
func WithinTransaction(f Func) Func {
return func(ctx context.Context, db *sql.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
if err := f(ctx, db); err != nil {
_ = tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
}

4
go.mod
View File

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

View File

@@ -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)
}

View File

@@ -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) {}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
View 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,
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
})
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -10,7 +10,7 @@ import (
type Arvelie string
func (a *Arvelie) isValid() bool {
func (a *Arvelie) IsValid() bool {
if a != nil {
return strings.EqualFold(string(*a), string(FromDate(ToDate(*a))))
}
@@ -29,7 +29,7 @@ func ToDate(a Arvelie) time.Time {
mon = (int(m[0]) - 65)
}
doty := (math.Floor(float64(mon)*14) + math.Floor(float64(d)) - 1)
doty := (math.Floor(float64(mon)*14) + float64(d) - 1)
yr, _ := strconv.Atoi(fmt.Sprintf("20%s", y))
return time.Date(yr, 1, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 0, int(doty))
}
@@ -42,17 +42,15 @@ func FromDate(date time.Time) Arvelie {
if doty == 365 || doty == 366 {
m = "+"
} else {
m = strings.ToUpper(string([]byte{byte(97 + math.Floor(float64(doty/14)))}))
m = strings.ToUpper(string([]byte{byte(97 + math.Floor(float64(doty)/14))}))
}
var d string
switch doty {
case 365:
d = fmt.Sprintf("%02d", 1)
break
case 366:
d = fmt.Sprintf("%02d", 2)
break
default:
d = fmt.Sprintf("%02d", (doty % 14))
}

View File

@@ -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
View File

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

View File

@@ -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) {