refactor: split go-pkg
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
package authreq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var SignatureLifetime = 30 * time.Minute
|
||||
var AuthHeader = "Authorization"
|
||||
|
||||
func Sign(req *http.Request, key ed25519.PrivateKey) (*http.Request, error) {
|
||||
pub := enc([]byte(key.Public().(ed25519.PublicKey)))
|
||||
|
||||
h := fnv.New128a()
|
||||
fmt.Fprint(h, req.Method, req.URL.String())
|
||||
|
||||
if req.Body != nil {
|
||||
b := &bytes.Buffer{}
|
||||
w := io.MultiWriter(h, b)
|
||||
_, err := io.Copy(w, req.Body)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.Body = io.NopCloser(b)
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.RegisteredClaims{
|
||||
Subject: enc(h.Sum(nil)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(SignatureLifetime)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: pub,
|
||||
})
|
||||
|
||||
sig, err := token.SignedString(key)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
req.Header.Set(AuthHeader, sig)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func Authorization(hdlr http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
auth := req.Header.Get(AuthHeader)
|
||||
if auth == "" {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
h := fnv.New128a()
|
||||
fmt.Fprint(h, req.Method, req.URL.String())
|
||||
|
||||
if req.Body != nil {
|
||||
b := &bytes.Buffer{}
|
||||
w := io.MultiWriter(h, b)
|
||||
_, err := io.Copy(w, req.Body)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
subject := enc(h.Sum(nil))
|
||||
token, err := jwt.ParseWithClaims(
|
||||
string(auth),
|
||||
&jwt.RegisteredClaims{},
|
||||
func(tok *jwt.Token) (any, error) {
|
||||
c, ok := tok.Claims.(*jwt.RegisteredClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("wrong type of claim")
|
||||
}
|
||||
|
||||
pub, err := dec(c.Issuer)
|
||||
return ed25519.PublicKey(pub), err
|
||||
},
|
||||
jwt.WithValidMethods([]string{jwt.SigningMethodEdDSA.Alg()}),
|
||||
jwt.WithJSONNumber(),
|
||||
)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
c, ok := token.Claims.(*jwt.RegisteredClaims)
|
||||
if !ok {
|
||||
rw.WriteHeader(http.StatusUnprocessableEntity)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
req = req.WithContext(context.WithValue(req.Context(), contextKey, c))
|
||||
|
||||
if c.Subject != subject {
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
func enc(b []byte) string {
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
func dec(s string) ([]byte, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
return base64.RawURLEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
var contextKey = struct{ name string }{"jwtClaim"}
|
||||
|
||||
func FromContext(ctx context.Context) *jwt.RegisteredClaims {
|
||||
if v := ctx.Value(contextKey); v != nil {
|
||||
if c, ok := v.(*jwt.RegisteredClaims); ok {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package authreq_test
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.sour.is/ev/pkg/authreq"
|
||||
)
|
||||
|
||||
func TestGETRequest(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(nil)
|
||||
is.NoErr(err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com/"+enc(pub)+"/test?q=test", nil)
|
||||
is.NoErr(err)
|
||||
|
||||
req, err = authreq.Sign(req, priv)
|
||||
is.NoErr(err)
|
||||
|
||||
t.Log(enc(pub))
|
||||
t.Log(req.Header.Get(authreq.AuthHeader))
|
||||
|
||||
var hdlr http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
c := authreq.FromContext(r.Context())
|
||||
if c == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(req.URL.Path, c.Issuer) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
hdlr = authreq.Authorization(hdlr)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
hdlr.ServeHTTP(rw, req)
|
||||
|
||||
is.Equal(rw.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestPOSTRequest(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
content := "this is post!"
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(nil)
|
||||
is.NoErr(err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/"+enc(pub)+"/test?q=test", strings.NewReader(content))
|
||||
is.NoErr(err)
|
||||
|
||||
req, err = authreq.Sign(req, priv)
|
||||
is.NoErr(err)
|
||||
|
||||
t.Log(enc(pub))
|
||||
t.Log(req.Header.Get(authreq.AuthHeader))
|
||||
|
||||
var hdlr http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
c := authreq.FromContext(r.Context())
|
||||
if c == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
contentCheck, err := io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
t.Log(string(contentCheck))
|
||||
if !strings.Contains(req.URL.Path, c.Issuer) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
hdlr = authreq.Authorization(hdlr)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
hdlr.ServeHTTP(rw, req)
|
||||
|
||||
is.Equal(rw.Code, http.StatusOK)
|
||||
|
||||
}
|
||||
|
||||
func enc(b []byte) string {
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
238
pkg/cache/cache.go
vendored
238
pkg/cache/cache.go
vendored
@@ -1,238 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
|
||||
DefaultEvictedBufferSize = 16
|
||||
)
|
||||
|
||||
// Cache is a thread-safe fixed size LRU cache.
|
||||
type Cache[K comparable, V any] struct {
|
||||
lru *LRU[K, V]
|
||||
evictedKeys []K
|
||||
evictedVals []V
|
||||
onEvictedCB func(ctx context.Context, k K, v V)
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates an LRU of the given size.
|
||||
func NewCache[K comparable, V any](size int) (*Cache[K, V], error) {
|
||||
return NewWithEvict[K, V](size, nil)
|
||||
}
|
||||
|
||||
// NewWithEvict constructs a fixed size cache with the given eviction
|
||||
// callback.
|
||||
func NewWithEvict[K comparable, V any](size int, onEvicted func(context.Context, K, V)) (c *Cache[K, V], err error) {
|
||||
// create a cache with default settings
|
||||
c = &Cache[K, V]{
|
||||
onEvictedCB: onEvicted,
|
||||
}
|
||||
if onEvicted != nil {
|
||||
c.initEvictBuffers()
|
||||
onEvicted = c.onEvicted
|
||||
}
|
||||
c.lru, err = NewLRU(size, onEvicted)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) initEvictBuffers() {
|
||||
c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
|
||||
c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
|
||||
}
|
||||
|
||||
// onEvicted save evicted key/val and sent in externally registered callback
|
||||
// outside of critical section
|
||||
func (c *Cache[K, V]) onEvicted(ctx context.Context, k K, v V) {
|
||||
c.evictedKeys = append(c.evictedKeys, k)
|
||||
c.evictedVals = append(c.evictedVals, v)
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *Cache[K, V]) Purge(ctx context.Context) {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
c.lru.Purge(ctx)
|
||||
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
|
||||
ks, vs = c.evictedKeys, c.evictedVals
|
||||
c.initEvictBuffers()
|
||||
}
|
||||
c.lock.Unlock()
|
||||
// invoke callback outside of critical section
|
||||
if c.onEvictedCB != nil {
|
||||
for i := 0; i < len(ks); i++ {
|
||||
c.onEvictedCB(ctx, ks[i], vs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *Cache[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Add(ctx, key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *Cache[K, V]) Get(key K) (value *V, ok bool) {
|
||||
c.lock.Lock()
|
||||
value, ok = c.lru.Get(key)
|
||||
c.lock.Unlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the
|
||||
// recent-ness or deleting it for being stale.
|
||||
func (c *Cache[K, V]) Contains(key K) bool {
|
||||
c.lock.RLock()
|
||||
containKey := c.lru.Contains(key)
|
||||
c.lock.RUnlock()
|
||||
return containKey
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *Cache[K, V]) Peek(key K) (value *V, ok bool) {
|
||||
c.lock.RLock()
|
||||
value, ok = c.lru.Peek(key)
|
||||
c.lock.RUnlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// ContainsOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache[K, V]) ContainsOrAdd(ctx context.Context, key K, value V) (ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
if c.lru.Contains(key) {
|
||||
c.lock.Unlock()
|
||||
return true, false
|
||||
}
|
||||
evicted = c.lru.Add(ctx, key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return false, evicted
|
||||
}
|
||||
|
||||
// PeekOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache[K, V]) PeekOrAdd(ctx context.Context, key K, value V) (previous *V, ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
previous, ok = c.lru.Peek(key)
|
||||
if ok {
|
||||
c.lock.Unlock()
|
||||
return previous, true, false
|
||||
}
|
||||
evicted = c.lru.Add(ctx, key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return nil, false, evicted
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache.
|
||||
func (c *Cache[K, V]) Remove(ctx context.Context, key K) (present bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
present = c.lru.Remove(ctx, key)
|
||||
if c.onEvictedCB != nil && present {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && present {
|
||||
c.onEvicted(ctx, k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *Cache[K, V]) Resize(ctx context.Context, size int) (evicted int) {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Resize(ctx, size)
|
||||
if c.onEvictedCB != nil && evicted > 0 {
|
||||
ks, vs = c.evictedKeys, c.evictedVals
|
||||
c.initEvictBuffers()
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted > 0 {
|
||||
for i := 0; i < len(ks); i++ {
|
||||
c.onEvictedCB(ctx, ks[i], vs[i])
|
||||
}
|
||||
}
|
||||
return evicted
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *Cache[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
key, value, ok = c.lru.RemoveOldest(ctx)
|
||||
if c.onEvictedCB != nil && ok {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && ok {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *Cache[K, V]) GetOldest() (key *K, value *V, ok bool) {
|
||||
c.lock.RLock()
|
||||
key, value, ok = c.lru.GetOldest()
|
||||
c.lock.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *Cache[K, V]) Keys() []K {
|
||||
c.lock.RLock()
|
||||
keys := c.lru.Keys()
|
||||
c.lock.RUnlock()
|
||||
return keys
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache[K, V]) Len() int {
|
||||
c.lock.RLock()
|
||||
length := c.lru.Len()
|
||||
c.lock.RUnlock()
|
||||
return length
|
||||
}
|
||||
131
pkg/cache/cache_test.go
vendored
131
pkg/cache/cache_test.go
vendored
@@ -1,131 +0,0 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.sour.is/ev/pkg/cache"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := cache.NewCache[string, int](1)
|
||||
is.NoErr(err)
|
||||
|
||||
evicted := c.Add(ctx, "one", 1)
|
||||
is.True(!evicted)
|
||||
|
||||
is.True(c.Contains("one"))
|
||||
_, ok := c.Peek("one")
|
||||
is.True(ok)
|
||||
|
||||
ok, evicted = c.ContainsOrAdd(ctx, "two", 2)
|
||||
is.True(!ok)
|
||||
is.True(evicted)
|
||||
|
||||
is.True(!c.Contains("one"))
|
||||
is.True(c.Contains("two"))
|
||||
|
||||
is.Equal(c.Len(), 1)
|
||||
is.Equal(c.Keys(), []string{"two"})
|
||||
|
||||
v, ok := c.Get("two")
|
||||
is.True(ok)
|
||||
is.Equal(*v, 2)
|
||||
|
||||
evictCount := c.Resize(ctx, 100)
|
||||
is.True(evictCount == 0)
|
||||
|
||||
c.Add(ctx, "one", 1)
|
||||
|
||||
prev, ok, evicted := c.PeekOrAdd(ctx, "three", 3)
|
||||
is.True(!ok)
|
||||
is.True(!evicted)
|
||||
is.Equal(prev, nil)
|
||||
|
||||
key, value, ok := c.GetOldest()
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
key, value, ok = c.RemoveOldest(ctx)
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
c.Remove(ctx, "one")
|
||||
|
||||
c.Purge(ctx)
|
||||
is.True(!c.Contains("three"))
|
||||
}
|
||||
|
||||
func TestCacheWithEvict(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
evictions := 0
|
||||
|
||||
c, err := cache.NewWithEvict(1, func(ctx context.Context, s string, i int) { evictions++ })
|
||||
is.NoErr(err)
|
||||
|
||||
key, value, ok := c.GetOldest()
|
||||
is.True(!ok)
|
||||
is.Equal(key, nil)
|
||||
is.Equal(value, nil)
|
||||
|
||||
key, value, ok = c.RemoveOldest(ctx)
|
||||
is.True(!ok)
|
||||
is.Equal(key, nil)
|
||||
is.Equal(value, nil)
|
||||
|
||||
evicted := c.Add(ctx, "one", 1)
|
||||
is.True(!evicted)
|
||||
|
||||
is.True(c.Contains("one"))
|
||||
_, ok = c.Peek("one")
|
||||
is.True(ok)
|
||||
|
||||
ok, evicted = c.ContainsOrAdd(ctx, "two", 2)
|
||||
is.True(!ok)
|
||||
is.True(evicted)
|
||||
|
||||
is.True(!c.Contains("one"))
|
||||
is.True(c.Contains("two"))
|
||||
|
||||
is.Equal(c.Len(), 1)
|
||||
is.Equal(c.Keys(), []string{"two"})
|
||||
|
||||
v, ok := c.Get("two")
|
||||
is.True(ok)
|
||||
is.Equal(*v, 2)
|
||||
|
||||
evictCount := c.Resize(ctx, 100)
|
||||
is.True(evictCount == 0)
|
||||
|
||||
c.Add(ctx, "one", 1)
|
||||
|
||||
prev, ok, evicted := c.PeekOrAdd(ctx, "three", 3)
|
||||
is.True(!ok)
|
||||
is.True(!evicted)
|
||||
is.Equal(prev, nil)
|
||||
|
||||
key, value, ok = c.GetOldest()
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
key, value, ok = c.RemoveOldest(ctx)
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
c.Resize(ctx, 1)
|
||||
|
||||
c.Purge(ctx)
|
||||
is.True(!c.Contains("three"))
|
||||
|
||||
is.Equal(evictions, 4)
|
||||
}
|
||||
235
pkg/cache/list.go
vendored
235
pkg/cache/list.go
vendored
@@ -1,235 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package list implements a doubly linked list.
|
||||
//
|
||||
// To iterate over a list (where l is a *List):
|
||||
//
|
||||
// for e := l.Front(); e != nil; e = e.Next() {
|
||||
// // do something with e.Value
|
||||
// }
|
||||
package cache
|
||||
|
||||
// Element is an element of a linked list.
|
||||
type Element[V any] struct {
|
||||
// Next and previous pointers in the doubly-linked list of elements.
|
||||
// To simplify the implementation, internally a list l is implemented
|
||||
// as a ring, such that &l.root is both the next element of the last
|
||||
// list element (l.Back()) and the previous element of the first list
|
||||
// element (l.Front()).
|
||||
next, prev *Element[V]
|
||||
|
||||
// The list to which this element belongs.
|
||||
list *List[V]
|
||||
|
||||
// The value stored with this element.
|
||||
Value V
|
||||
}
|
||||
|
||||
// Next returns the next list element or nil.
|
||||
func (e *Element[V]) Next() *Element[V] {
|
||||
if p := e.next; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prev returns the previous list element or nil.
|
||||
func (e *Element[V]) Prev() *Element[V] {
|
||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List represents a doubly linked list.
|
||||
// The zero value for List is an empty list ready to use.
|
||||
type List[V any] struct {
|
||||
root Element[V] // sentinel list element, only &root, root.prev, and root.next are used
|
||||
len int // current list length excluding (this) sentinel element
|
||||
}
|
||||
|
||||
// Init initializes or clears list l.
|
||||
func (l *List[V]) Init() *List[V] {
|
||||
l.root.next = &l.root
|
||||
l.root.prev = &l.root
|
||||
l.len = 0
|
||||
return l
|
||||
}
|
||||
|
||||
// NewList returns an initialized list.
|
||||
func NewList[V any]() *List[V] { return new(List[V]).Init() }
|
||||
|
||||
// Len returns the number of elements of list l.
|
||||
// The complexity is O(1).
|
||||
func (l *List[V]) Len() int { return l.len }
|
||||
|
||||
// Front returns the first element of list l or nil if the list is empty.
|
||||
func (l *List[V]) Front() *Element[V] {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.next
|
||||
}
|
||||
|
||||
// Back returns the last element of list l or nil if the list is empty.
|
||||
func (l *List[V]) Back() *Element[V] {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.prev
|
||||
}
|
||||
|
||||
// lazyInit lazily initializes a zero List value.
|
||||
func (l *List[V]) lazyInit() {
|
||||
if l.root.next == nil {
|
||||
l.Init()
|
||||
}
|
||||
}
|
||||
|
||||
// insert inserts e after at, increments l.len, and returns e.
|
||||
func (l *List[V]) insert(e, at *Element[V]) *Element[V] {
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
e.list = l
|
||||
l.len++
|
||||
return e
|
||||
}
|
||||
|
||||
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
|
||||
func (l *List[V]) insertValue(v V, at *Element[V]) *Element[V] {
|
||||
return l.insert(&Element[V]{Value: v}, at)
|
||||
}
|
||||
|
||||
// remove removes e from its list, decrements l.len
|
||||
func (l *List[V]) remove(e *Element[V]) {
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
e.next = nil // avoid memory leaks
|
||||
e.prev = nil // avoid memory leaks
|
||||
e.list = nil
|
||||
l.len--
|
||||
}
|
||||
|
||||
// move moves e to next to at.
|
||||
func (l *List[V]) move(e, at *Element[V]) {
|
||||
if e == at {
|
||||
return
|
||||
}
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
}
|
||||
|
||||
// Remove removes e from l if e is an element of list l.
|
||||
// It returns the element value e.Value.
|
||||
// The element must not be nil.
|
||||
func (l *List[V]) Remove(e *Element[V]) any {
|
||||
if e.list == l {
|
||||
// if e.list == l, l must have been initialized when e was inserted
|
||||
// in l or l == nil (e is a zero Element) and l.remove will crash
|
||||
l.remove(e)
|
||||
}
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
||||
func (l *List[V]) PushFront(v V) *Element[V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(v, &l.root)
|
||||
}
|
||||
|
||||
// PushBack inserts a new element e with value v at the back of list l and returns e.
|
||||
func (l *List[V]) PushBack(v V) *Element[V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(v, l.root.prev)
|
||||
}
|
||||
|
||||
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
|
||||
// If mark is not an element of l, the list is not modified.
|
||||
// The mark must not be nil.
|
||||
func (l *List[V]) InsertBefore(v V, mark *Element[V]) *Element[V] {
|
||||
if mark.list != l {
|
||||
return nil
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
return l.insertValue(v, mark.prev)
|
||||
}
|
||||
|
||||
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
|
||||
// If mark is not an element of l, the list is not modified.
|
||||
// The mark must not be nil.
|
||||
func (l *List[V]) InsertAfter(v V, mark *Element[V]) *Element[V] {
|
||||
if mark.list != l {
|
||||
return nil
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
return l.insertValue(v, mark)
|
||||
}
|
||||
|
||||
// MoveToFront moves element e to the front of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
// The element must not be nil.
|
||||
func (l *List[V]) MoveToFront(e *Element[V]) {
|
||||
if e.list != l || l.root.next == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.move(e, &l.root)
|
||||
}
|
||||
|
||||
// MoveToBack moves element e to the back of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
// The element must not be nil.
|
||||
func (l *List[V]) MoveToBack(e *Element[V]) {
|
||||
if e.list != l || l.root.prev == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.move(e, l.root.prev)
|
||||
}
|
||||
|
||||
// MoveBefore moves element e to its new position before mark.
|
||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
||||
// The element and mark must not be nil.
|
||||
func (l *List[V]) MoveBefore(e, mark *Element[V]) {
|
||||
if e.list != l || e == mark || mark.list != l {
|
||||
return
|
||||
}
|
||||
l.move(e, mark.prev)
|
||||
}
|
||||
|
||||
// MoveAfter moves element e to its new position after mark.
|
||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
||||
// The element and mark must not be nil.
|
||||
func (l *List[V]) MoveAfter(e, mark *Element[V]) {
|
||||
if e.list != l || e == mark || mark.list != l {
|
||||
return
|
||||
}
|
||||
l.move(e, mark)
|
||||
}
|
||||
|
||||
// PushBackList inserts a copy of another list at the back of list l.
|
||||
// The lists l and other may be the same. They must not be nil.
|
||||
func (l *List[V]) PushBackList(other *List[V]) {
|
||||
l.lazyInit()
|
||||
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
|
||||
l.insertValue(e.Value, l.root.prev)
|
||||
}
|
||||
}
|
||||
|
||||
// PushFrontList inserts a copy of another list at the front of list l.
|
||||
// The lists l and other may be the same. They must not be nil.
|
||||
func (l *List[V]) PushFrontList(other *List[V]) {
|
||||
l.lazyInit()
|
||||
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
|
||||
l.insertValue(e.Value, &l.root)
|
||||
}
|
||||
}
|
||||
175
pkg/cache/lru.go
vendored
175
pkg/cache/lru.go
vendored
@@ -1,175 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||
type EvictCallback[K comparable, V any] func(context.Context, K, V)
|
||||
|
||||
// LRU implements a non-thread safe fixed size LRU cache
|
||||
type LRU[K comparable, V any] struct {
|
||||
size int
|
||||
evictList *List[entry[K, V]]
|
||||
items map[K]*Element[entry[K, V]]
|
||||
onEvict EvictCallback[K, V]
|
||||
}
|
||||
|
||||
// entry is used to hold a value in the evictList
|
||||
type entry[K comparable, V any] struct {
|
||||
key K
|
||||
value V
|
||||
}
|
||||
|
||||
// NewLRU constructs an LRU of the given size
|
||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
|
||||
if size <= 0 {
|
||||
return nil, errors.New("must provide a positive size")
|
||||
}
|
||||
c := &LRU[K, V]{
|
||||
size: size,
|
||||
evictList: NewList[entry[K, V]](),
|
||||
items: make(map[K]*Element[entry[K, V]]),
|
||||
onEvict: onEvict,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *LRU[K, V]) Purge(ctx context.Context) {
|
||||
for k, v := range c.items {
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(ctx, k, v.Value.value)
|
||||
}
|
||||
delete(c.items, k)
|
||||
}
|
||||
c.evictList.Init()
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *LRU[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
ent.Value.value = value
|
||||
return false
|
||||
}
|
||||
|
||||
// Add new item
|
||||
entry := c.evictList.PushFront(entry[K, V]{key, value})
|
||||
c.items[key] = entry
|
||||
|
||||
evict := c.evictList.Len() > c.size
|
||||
// Verify size not exceeded
|
||||
if evict {
|
||||
c.removeOldest(ctx)
|
||||
}
|
||||
return evict
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *LRU[K, V]) Get(key K) (value *V, ok bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
if ent == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &ent.Value.value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
||||
// or deleting it for being stale.
|
||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
||||
_, ok = c.items[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *LRU[K, V]) Peek(key K) (value *V, ok bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
return &ent.Value.value, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache, returning if the
|
||||
// key was contained.
|
||||
func (c *LRU[K, V]) Remove(ctx context.Context, key K) (present bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.removeElement(ctx, ent)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *LRU[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
c.removeElement(ctx, ent)
|
||||
kv := ent.Value
|
||||
return &kv.key, &kv.value, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *LRU[K, V]) GetOldest() (key *K, value *V, ok bool) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
kv := ent.Value
|
||||
return &kv.key, &kv.value, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *LRU[K, V]) Keys() []K {
|
||||
keys := make([]K, len(c.items))
|
||||
i := 0
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
||||
keys[i] = ent.Value.key
|
||||
i++
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *LRU[K, V]) Len() int {
|
||||
return c.evictList.Len()
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *LRU[K, V]) Resize(ctx context.Context, size int) (evicted int) {
|
||||
diff := c.Len() - size
|
||||
if diff < 0 {
|
||||
diff = 0
|
||||
}
|
||||
for i := 0; i < diff; i++ {
|
||||
c.removeOldest(ctx)
|
||||
}
|
||||
c.size = size
|
||||
return diff
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest item from the cache.
|
||||
func (c *LRU[K, V]) removeOldest(ctx context.Context) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
c.removeElement(ctx, ent)
|
||||
}
|
||||
}
|
||||
|
||||
// removeElement is used to remove a given list element from the cache
|
||||
func (c *LRU[K, V]) removeElement(ctx context.Context, e *Element[entry[K, V]]) {
|
||||
c.evictList.Remove(e)
|
||||
kv := e.Value
|
||||
delete(c.items, kv.key)
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(ctx, kv.key, kv.value)
|
||||
}
|
||||
}
|
||||
160
pkg/cron/cron.go
160
pkg/cron/cron.go
@@ -1,160 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/locker"
|
||||
"go.sour.is/ev/pkg/set"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type task func(context.Context, time.Time) error
|
||||
type job struct {
|
||||
Month, Weekday, Day,
|
||||
Hour, Minute, Second *set.BoundSet[int8]
|
||||
Task task
|
||||
}
|
||||
|
||||
var DefaultGranularity = time.Minute
|
||||
|
||||
type state struct {
|
||||
queue []task
|
||||
}
|
||||
type cron struct {
|
||||
jobs []job
|
||||
state *locker.Locked[state]
|
||||
granularity time.Duration
|
||||
}
|
||||
|
||||
func New(granularity time.Duration) *cron {
|
||||
return &cron{granularity: granularity, state: locker.New(&state{})}
|
||||
}
|
||||
|
||||
func parseInto(c string, s *set.BoundSet[int8]) *set.BoundSet[int8] {
|
||||
if c == "*" || c == "" {
|
||||
s.AddRange(0, 100)
|
||||
}
|
||||
for _, split := range strings.Split(c, ",") {
|
||||
minmax := strings.SplitN(split, "-", 2)
|
||||
switch len(minmax) {
|
||||
case 2:
|
||||
min, _ := strconv.ParseInt(minmax[0], 10, 8)
|
||||
max, _ := strconv.ParseInt(minmax[1], 10, 8)
|
||||
s.AddRange(int8(min), int8(max))
|
||||
default:
|
||||
min, _ := strconv.ParseInt(minmax[0], 10, 8)
|
||||
s.Add(int8(min))
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// This function creates a new job that occurs at the given day and the given
|
||||
// 24hour time. Any of the values may be -1 as an "any" match, so passing in
|
||||
// a day of -1, the event occurs every day; passing in a second value of -1, the
|
||||
// event will fire every second that the other parameters match.
|
||||
func (c *cron) NewCron(expr string, task func(context.Context, time.Time) error) {
|
||||
sp := append(strings.Fields(expr), make([]string, 5)...)[:5]
|
||||
|
||||
job := job{
|
||||
Month: parseInto(sp[4], set.NewBoundSet[int8](1, 12)),
|
||||
Weekday: parseInto(sp[3], set.NewBoundSet[int8](0, 6)),
|
||||
Day: parseInto(sp[2], set.NewBoundSet[int8](1, 31)),
|
||||
Hour: parseInto(sp[1], set.NewBoundSet[int8](0, 23)),
|
||||
Minute: parseInto(sp[0], set.NewBoundSet[int8](0, 59)),
|
||||
Task: task,
|
||||
}
|
||||
c.jobs = append(c.jobs, job)
|
||||
}
|
||||
func (c *cron) RunOnce(ctx context.Context, once func(context.Context, time.Time) error) {
|
||||
c.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||
state.queue = append(state.queue, once)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (cj job) Matches(t time.Time) (ok bool) {
|
||||
return cj.Month.Has(int8(t.Month())) &&
|
||||
cj.Day.Has(int8(t.Day())) &&
|
||||
cj.Weekday.Has(int8(t.Weekday()%7)) &&
|
||||
cj.Hour.Has(int8(t.Hour())) &&
|
||||
cj.Minute.Has(int8(t.Minute()))
|
||||
}
|
||||
|
||||
func (cj job) String() string {
|
||||
return fmt.Sprintf("job[\n m:%s\n h:%s\n d:%s\n w:%s\n M:%s\n]", cj.Minute, cj.Hour, cj.Day, cj.Weekday, cj.Month)
|
||||
}
|
||||
|
||||
func (c *cron) Run(ctx context.Context) error {
|
||||
tick := time.NewTicker(c.granularity)
|
||||
defer tick.Stop()
|
||||
|
||||
go c.run(ctx, time.Now())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case now := <-tick.C:
|
||||
// fmt.Println(now.Second(), now.Hour(), now.Day(), int8(now.Weekday()), uint8(now.Month()))
|
||||
go c.run(ctx, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cron) run(ctx context.Context, now time.Time) {
|
||||
var run []task
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
// Add Jitter
|
||||
timer := time.NewTimer(time.Duration(rand.Intn(300)) * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
span.AddEvent("Cron Run: " + now.Format(time.RFC822))
|
||||
// fmt.Println("Cron Run: ", now.Format(time.RFC822))
|
||||
|
||||
c.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||
run = append(run, state.queue...)
|
||||
state.queue = state.queue[:0]
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, j := range c.jobs {
|
||||
if j.Matches(now) {
|
||||
span.AddEvent(j.String())
|
||||
run = append(run, j.Task)
|
||||
}
|
||||
}
|
||||
|
||||
if len(run) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
wg, _ := errgroup.WithContext(ctx)
|
||||
|
||||
for i := range run {
|
||||
fn := run[i]
|
||||
wg.Go(func() error { return fn(ctx, now) })
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.String("tick", now.String()),
|
||||
attribute.Int("count", len(run)),
|
||||
)
|
||||
|
||||
err := wg.Wait()
|
||||
span.RecordError(err)
|
||||
}
|
||||
@@ -13,16 +13,17 @@ import (
|
||||
|
||||
"github.com/tidwall/wal"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"go.sour.is/pkg/cache"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
"go.sour.is/pkg/math"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/cache"
|
||||
"go.sour.is/ev/pkg/es/driver"
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/locker"
|
||||
"go.sour.is/ev/pkg/math"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
const CachSize = 1000
|
||||
@@ -35,10 +36,10 @@ type diskStore struct {
|
||||
path string
|
||||
openlogs *locker.Locked[openlogs]
|
||||
|
||||
m_disk_open syncint64.Counter
|
||||
m_disk_evict syncint64.Counter
|
||||
m_disk_read syncint64.Counter
|
||||
m_disk_write syncint64.Counter
|
||||
m_disk_open metric.Int64Counter
|
||||
m_disk_evict metric.Int64Counter
|
||||
m_disk_read metric.Int64Counter
|
||||
m_disk_write metric.Int64Counter
|
||||
}
|
||||
|
||||
const AppendOnly = ev.AppendOnly
|
||||
@@ -53,16 +54,16 @@ func Init(ctx context.Context) error {
|
||||
m := lg.Meter(ctx)
|
||||
var err, errs error
|
||||
|
||||
d.m_disk_open, err = m.SyncInt64().Counter("disk_open")
|
||||
d.m_disk_open, err = m.Int64Counter("disk_open")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_evict, err = m.SyncInt64().Counter("disk_evict")
|
||||
d.m_disk_evict, err = m.Int64Counter("disk_evict")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_read, err = m.SyncInt64().Counter("disk_read")
|
||||
d.m_disk_read, err = m.Int64Counter("disk_read")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_write, err = m.SyncInt64().Counter("disk_write")
|
||||
d.m_disk_write, err = m.Int64Counter("disk_write")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
ev.Register(ctx, "file", d)
|
||||
@@ -4,7 +4,7 @@ package driver
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
type Driver interface {
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
"go.sour.is/pkg/math"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/es/driver"
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/locker"
|
||||
"go.sour.is/ev/pkg/math"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/es/driver"
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
type projector struct {
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/es/driver"
|
||||
"go.sour.is/ev/pkg/es/driver/projecter"
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/driver/projecter"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
type mockDriver struct {
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"errors"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/es/driver"
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/pkg/lg"
|
||||
)
|
||||
|
||||
type resolvelinks struct {
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/es/driver"
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/locker"
|
||||
"go.sour.is/ev/pkg/driver"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
40
pkg/env/env.go
vendored
40
pkg/env/env.go
vendored
@@ -1,40 +0,0 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Default(name, defaultValue string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
defaultValue = strings.TrimSpace(defaultValue)
|
||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
||||
log.Println("# ", name, "=", v)
|
||||
return v
|
||||
}
|
||||
log.Println("# ", name, "=", defaultValue, "(default)")
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
type secret string
|
||||
|
||||
func (s secret) String() string {
|
||||
if s == "" {
|
||||
return "(nil)"
|
||||
}
|
||||
return "***"
|
||||
}
|
||||
func (s secret) Secret() string {
|
||||
return string(s)
|
||||
}
|
||||
func Secret(name, defaultValue string) secret {
|
||||
name = strings.TrimSpace(name)
|
||||
defaultValue = strings.TrimSpace(defaultValue)
|
||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
||||
log.Println("# ", name, "=", secret(v))
|
||||
return secret(v)
|
||||
}
|
||||
log.Println("# ", name, "=", secret(defaultValue), "(default)")
|
||||
return secret(defaultValue)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
type Meta @goModel(model: "go.sour.is/ev/pkg/es/event.Meta") {
|
||||
type Meta @goModel(model: "go.sour.is/ev/pkg/event.Meta") {
|
||||
eventID: String! @goField(name: "getEventID")
|
||||
streamID: String! @goField(name: "ActualStreamID")
|
||||
position: Int! @goField(name: "ActualPosition")
|
||||
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.sour.is/pkg/gql"
|
||||
"go.sour.is/pkg/lg"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/gql"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
type EventResolver interface {
|
||||
|
||||
@@ -3,7 +3,7 @@ package event_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
type Agg struct {
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/matryer/is"
|
||||
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
"go.sour.is/ev/pkg/event"
|
||||
)
|
||||
|
||||
type DummyEvent struct {
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/locker"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -1,37 +0,0 @@
|
||||
scalar Time
|
||||
scalar Map
|
||||
|
||||
type Connection @goModel(model: "go.sour.is/ev/pkg/gql.Connection") {
|
||||
paging: PageInfo!
|
||||
edges: [Edge!]!
|
||||
}
|
||||
input PageInput @goModel(model: "go.sour.is/ev/pkg/gql.PageInput") {
|
||||
after: Int = 0
|
||||
before: Int
|
||||
count: Int = 30
|
||||
}
|
||||
type PageInfo @goModel(model: "go.sour.is/ev/pkg/gql.PageInfo") {
|
||||
next: Boolean!
|
||||
prev: Boolean!
|
||||
|
||||
begin: Int!
|
||||
end: Int!
|
||||
}
|
||||
interface Edge @goModel(model: "go.sour.is/ev/pkg/gql.Edge"){
|
||||
id: ID!
|
||||
}
|
||||
|
||||
directive @goModel(
|
||||
model: String
|
||||
models: [String!]
|
||||
) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
|
||||
|
||||
directive @goField(
|
||||
forceResolver: Boolean
|
||||
name: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
||||
|
||||
directive @goTag(
|
||||
key: String!
|
||||
value: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
||||
@@ -1,67 +0,0 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"go.sour.is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type Edge interface {
|
||||
IsEdge()
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
Paging *PageInfo `json:"paging"`
|
||||
Edges []Edge `json:"edges"`
|
||||
}
|
||||
|
||||
type PostEvent struct {
|
||||
ID string `json:"id"`
|
||||
Payload string `json:"payload"`
|
||||
Tags []string `json:"tags"`
|
||||
Meta *event.Meta `json:"meta"`
|
||||
}
|
||||
|
||||
func (PostEvent) IsEdge() {}
|
||||
|
||||
func (e *PostEvent) PayloadJSON(ctx context.Context) (m map[string]interface{}, err error) {
|
||||
err = json.Unmarshal([]byte(e.Payload), &m)
|
||||
return
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Next bool `json:"next"`
|
||||
Prev bool `json:"prev"`
|
||||
Begin uint64 `json:"begin"`
|
||||
End uint64 `json:"end"`
|
||||
}
|
||||
|
||||
type PageInput struct {
|
||||
After *int64 `json:"after"`
|
||||
Before *int64 `json:"before"`
|
||||
Count *int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (p *PageInput) GetIdx(v int64) int64 {
|
||||
if p == nil {
|
||||
// pass
|
||||
} else if p.Before != nil {
|
||||
return (*p.Before)
|
||||
} else if p.After != nil {
|
||||
return *p.After
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
func (p *PageInput) GetCount(v int64) int64 {
|
||||
if p == nil || p.Count == nil {
|
||||
return v
|
||||
} else if p.Before != nil {
|
||||
return -(*p.Count)
|
||||
} else if p.After != nil {
|
||||
return *p.Count
|
||||
}
|
||||
|
||||
return *p.Count
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package gql
|
||||
|
||||
import "context"
|
||||
|
||||
func ToContext[K comparable, V any](ctx context.Context, key K, value V) context.Context {
|
||||
return context.WithValue(ctx, key, value)
|
||||
}
|
||||
func FromContext[K comparable, V any](ctx context.Context, key K) V {
|
||||
var empty V
|
||||
if v, ok := ctx.Value(key).(V); ok {
|
||||
return v
|
||||
}
|
||||
return empty
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package graphiql
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{.title}}</title>
|
||||
<style>
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#graphiql {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/react@{{.reactVersion}}/umd/react.production.min.js"
|
||||
integrity="{{.reactSRI}}"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/react-dom@{{.reactVersion}}/umd/react-dom.production.min.js"
|
||||
integrity="{{.reactDOMSRI}}"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.css"
|
||||
x-integrity="{{.cssSRI}}"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.js"
|
||||
x-integrity="{{.jsSRI}}"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<script>
|
||||
{{- if .endpointIsAbsolute}}
|
||||
const url = {{.endpoint}};
|
||||
const subscriptionUrl = {{.subscriptionEndpoint}};
|
||||
{{- else}}
|
||||
const url = location.protocol + '//' + location.host + {{.endpoint}};
|
||||
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:';
|
||||
const subscriptionUrl = wsProto + '//' + location.host + {{.endpoint}};
|
||||
{{- end}}
|
||||
|
||||
const fetcher = GraphiQL.createFetcher({ url, subscriptionUrl });
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: fetcher,
|
||||
isHeadersEditorEnabled: true,
|
||||
shouldPersistHeaders: true
|
||||
}),
|
||||
document.getElementById('graphiql'),
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
// Handler responsible for setting up the playground
|
||||
func Handler(title string, endpoint string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html; charset=UTF-8")
|
||||
err := page.Execute(w, map[string]interface{}{
|
||||
"title": title,
|
||||
"endpoint": endpoint,
|
||||
"endpointIsAbsolute": endpointHasScheme(endpoint),
|
||||
"subscriptionEndpoint": getSubscriptionEndpoint(endpoint),
|
||||
"version": "2.4.1",
|
||||
"reactVersion": "17.0.2",
|
||||
"cssSRI": "sha256-bGeEsMhcAqeXBjh2w0eQzBTFAxwlxhM0PKIKqMshlnk=",
|
||||
"jsSRI": "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=",
|
||||
"reactSRI": "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=",
|
||||
"reactDOMSRI": "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endpointHasScheme checks if the endpoint has a scheme.
|
||||
func endpointHasScheme(endpoint string) bool {
|
||||
u, err := url.Parse(endpoint)
|
||||
return err == nil && u.Scheme != ""
|
||||
}
|
||||
|
||||
// getSubscriptionEndpoint returns the subscription endpoint for the given
|
||||
// endpoint if it is parsable as a URL, or an empty string.
|
||||
func getSubscriptionEndpoint(endpoint string) string {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "https":
|
||||
u.Scheme = "wss"
|
||||
default:
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8/>
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
|
||||
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
|
||||
integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
|
||||
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
|
||||
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
|
||||
integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
|
||||
<title>{{.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<style type="text/css">
|
||||
html { font-family: "Open Sans", sans-serif; overflow: hidden; }
|
||||
body { margin: 0; background: #172a3a; }
|
||||
</style>
|
||||
<div id="root"/>
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function (event) {
|
||||
const root = document.getElementById('root');
|
||||
root.classList.add('playgroundIn');
|
||||
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:'
|
||||
GraphQLPlayground.init(root, {
|
||||
endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
|
||||
subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
|
||||
shareEnabled: true,
|
||||
settings: {
|
||||
'request.credentials': 'same-origin'
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
func Handler(title string, endpoint string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
err := page.Execute(w, map[string]string{
|
||||
"title": title,
|
||||
"endpoint": endpoint,
|
||||
"version": "1.7.28",
|
||||
"cssSRI": "sha256-dKnNLEFwKSVFpkpjRWe+o/jQDM6n/JsvQ0J3l5Dk3fc=",
|
||||
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
|
||||
"jsSRI": "sha256-VVwEZwxs4qS5W7E+/9nXINYgr/BJRWKOi/rTMUdmmWg=",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/lru"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/ravilushqa/otelgqlgen"
|
||||
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/gql/graphiql"
|
||||
"go.sour.is/ev/pkg/gql/playground"
|
||||
)
|
||||
|
||||
type BaseResolver interface {
|
||||
ExecutableSchema() graphql.ExecutableSchema
|
||||
BaseResolver() IsResolver
|
||||
}
|
||||
|
||||
type Resolver[T BaseResolver] struct {
|
||||
res T
|
||||
CheckOrigin func(r *http.Request) bool
|
||||
}
|
||||
type IsResolver interface {
|
||||
IsResolver()
|
||||
}
|
||||
|
||||
var defaultCheckOrign = func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func New[T BaseResolver](ctx context.Context, base T, resolvers ...IsResolver) (*Resolver[T], error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
noop := reflect.ValueOf(base.BaseResolver())
|
||||
|
||||
v := reflect.ValueOf(base)
|
||||
v = reflect.Indirect(v)
|
||||
|
||||
outer:
|
||||
for _, idx := range reflect.VisibleFields(v.Type()) {
|
||||
field := v.FieldByIndex(idx.Index)
|
||||
|
||||
for i := range resolvers {
|
||||
rs := reflect.ValueOf(resolvers[i])
|
||||
|
||||
if field.IsNil() && rs.Type().Implements(field.Type()) {
|
||||
// log.Print("found ", field.Type().Name())
|
||||
span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
|
||||
field.Set(rs)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
// log.Print(fmt.Sprint("default ", field.Type().Name()))
|
||||
span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
|
||||
field.Set(noop)
|
||||
}
|
||||
|
||||
return &Resolver[T]{res: base, CheckOrigin: defaultCheckOrign}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver[T]) Resolver() T {
|
||||
return r.res
|
||||
}
|
||||
|
||||
// ChainMiddlewares will check all embeded resolvers for a GetMiddleware func and add to handler.
|
||||
func (r *Resolver[T]) ChainMiddlewares(h http.Handler) http.Handler {
|
||||
v := reflect.ValueOf(r.Resolver()) // Get reflected value of *Resolver
|
||||
v = reflect.Indirect(v) // Get the pointed value (returns a zero value on nil)
|
||||
for _, idx := range reflect.VisibleFields(v.Type()) {
|
||||
field := v.FieldByIndex(idx.Index)
|
||||
// log.Print("middleware ", field.Type().Name())
|
||||
|
||||
if !field.CanInterface() { // Skip non-interface types.
|
||||
continue
|
||||
}
|
||||
if iface, ok := field.Interface().(interface {
|
||||
GetMiddleware() func(http.Handler) http.Handler
|
||||
}); ok {
|
||||
h = iface.GetMiddleware()(h) // Append only items that fulfill the interface.
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (r *Resolver[T]) RegisterHTTP(mux *http.ServeMux) {
|
||||
gql := NewServer(r.Resolver().ExecutableSchema(), r.CheckOrigin)
|
||||
gql.SetRecoverFunc(NoopRecover)
|
||||
gql.Use(otelgqlgen.Middleware())
|
||||
mux.Handle("/gql", lg.Htrace(r.ChainMiddlewares(gql), "gql"))
|
||||
mux.Handle("/graphiql", graphiql.Handler("GraphiQL playground", "/gql"))
|
||||
mux.Handle("/playground", playground.Handler("GraphQL playground", "/gql"))
|
||||
}
|
||||
|
||||
func NoopRecover(ctx context.Context, err interface{}) error {
|
||||
if err, ok := err.(string); ok && err == "not implemented" {
|
||||
return gqlerror.Errorf("not implemented")
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
debug.PrintStack()
|
||||
|
||||
return gqlerror.Errorf("internal system error")
|
||||
}
|
||||
|
||||
func NewServer(es graphql.ExecutableSchema, checkOrigin func(*http.Request) bool) *handler.Server {
|
||||
srv := handler.New(es)
|
||||
|
||||
srv.AddTransport(transport.Websocket{
|
||||
Upgrader: websocket.Upgrader{
|
||||
CheckOrigin: checkOrigin,
|
||||
},
|
||||
KeepAlivePingInterval: 10 * time.Second,
|
||||
})
|
||||
srv.AddTransport(transport.Options{})
|
||||
srv.AddTransport(transport.GET{})
|
||||
srv.AddTransport(transport.POST{})
|
||||
srv.AddTransport(transport.MultipartForm{})
|
||||
|
||||
srv.SetQueryCache(lru.New(1000))
|
||||
|
||||
srv.Use(extension.Introspection{})
|
||||
srv.Use(extension.AutomaticPersistedQuery{
|
||||
Cache: lru.New(100),
|
||||
})
|
||||
|
||||
return srv
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package locker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
)
|
||||
|
||||
type Locked[T any] struct {
|
||||
state chan *T
|
||||
}
|
||||
|
||||
// New creates a new locker for the given value.
|
||||
func New[T any](initial *T) *Locked[T] {
|
||||
s := &Locked[T]{}
|
||||
s.state = make(chan *T, 1)
|
||||
s.state <- initial
|
||||
return s
|
||||
}
|
||||
|
||||
type ctxKey struct{ name string }
|
||||
|
||||
// Use will call the function with the locked value
|
||||
func (s *Locked[T]) Use(ctx context.Context, fn func(context.Context, *T) error) error {
|
||||
if s == nil {
|
||||
return fmt.Errorf("locker not initialized")
|
||||
}
|
||||
|
||||
key := ctxKey{fmt.Sprintf("%p", s)}
|
||||
|
||||
if value := ctx.Value(key); value != nil {
|
||||
return fmt.Errorf("%w: %T", ErrNested, s)
|
||||
}
|
||||
ctx = context.WithValue(ctx, key, key)
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var t T
|
||||
span.SetAttributes(
|
||||
attribute.String("typeOf", fmt.Sprintf("%T", t)),
|
||||
)
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
select {
|
||||
case state := <-s.state:
|
||||
defer func() { s.state <- state }()
|
||||
return fn(ctx, state)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Copy will return a shallow copy of the locked object.
|
||||
func (s *Locked[T]) Copy(ctx context.Context) (T, error) {
|
||||
var t T
|
||||
|
||||
err := s.Use(ctx, func(ctx context.Context, c *T) error {
|
||||
if c != nil {
|
||||
t = *c
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return t, err
|
||||
}
|
||||
|
||||
var ErrNested = errors.New("nested locker call")
|
||||
@@ -1,96 +0,0 @@
|
||||
package locker_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
|
||||
"go.sour.is/ev/pkg/locker"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Value string
|
||||
Counter int
|
||||
}
|
||||
|
||||
func TestLocker(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
value := locker.New(&config{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err := value.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
c.Value = "one"
|
||||
c.Counter++
|
||||
return nil
|
||||
})
|
||||
is.NoErr(err)
|
||||
|
||||
c, err := value.Copy(context.Background())
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(c.Value, "one")
|
||||
is.Equal(c.Counter, 1)
|
||||
|
||||
wait := make(chan struct{})
|
||||
|
||||
go value.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
c.Value = "two"
|
||||
c.Counter++
|
||||
close(wait)
|
||||
return nil
|
||||
})
|
||||
|
||||
<-wait
|
||||
cancel()
|
||||
|
||||
err = value.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
c.Value = "three"
|
||||
c.Counter++
|
||||
return nil
|
||||
})
|
||||
is.True(err != nil)
|
||||
|
||||
c, err = value.Copy(context.Background())
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(c.Value, "two")
|
||||
is.Equal(c.Counter, 2)
|
||||
}
|
||||
|
||||
func TestNestedLocker(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
value := locker.New(&config{})
|
||||
other := locker.New(&config{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err := value.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
return value.Use(ctx, func(ctx context.Context, t *config) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
is.True(errors.Is(err, locker.ErrNested))
|
||||
|
||||
err = value.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
return other.Use(ctx, func(ctx context.Context, t *config) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
is.NoErr(err)
|
||||
|
||||
err = value.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
return other.Use(ctx, func(ctx context.Context, t *config) error {
|
||||
return value.Use(ctx, func(ctx context.Context, x *config) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
is.True(errors.Is(err, locker.ErrNested))
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package math
|
||||
|
||||
type signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
type unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
type integer interface {
|
||||
signed | unsigned
|
||||
}
|
||||
type float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
type ordered interface {
|
||||
integer | float | ~string
|
||||
}
|
||||
|
||||
func Abs[T signed](i T) T {
|
||||
if i > 0 {
|
||||
return i
|
||||
}
|
||||
return -i
|
||||
}
|
||||
func Max[T ordered](i T, candidates ...T) T {
|
||||
for _, j := range candidates {
|
||||
if i < j {
|
||||
i = j
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
func Min[T ordered](i T, candidates ...T) T {
|
||||
for _, j := range candidates {
|
||||
if i > j {
|
||||
i = j
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func PagerBox(first, last uint64, pos, count int64) (uint64, int64) {
|
||||
var start uint64
|
||||
|
||||
if pos >= 0 {
|
||||
if int64(first) > pos {
|
||||
start = first
|
||||
} else {
|
||||
start = uint64(pos) + 1
|
||||
}
|
||||
} else {
|
||||
start = uint64(int64(last) + pos + 1)
|
||||
}
|
||||
|
||||
switch {
|
||||
case count > 0:
|
||||
count = Min(count, int64(last-start)+1)
|
||||
|
||||
case pos >= 0 && count < 0:
|
||||
count = Max(count, int64(first-start))
|
||||
|
||||
case pos < 0 && count < 0:
|
||||
count = Max(count, int64(first-start)-1)
|
||||
}
|
||||
|
||||
if count == 0 || (start < first && count <= 0) || (start > last && count >= 0) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
return start, count
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package math_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/pkg/math"
|
||||
)
|
||||
|
||||
func TestMath(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
is.Equal(5, math.Abs(-5))
|
||||
is.Equal(math.Abs(5), math.Abs(-5))
|
||||
|
||||
is.Equal(10, math.Max(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
is.Equal(1, math.Min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
|
||||
is.Equal(1, math.Min(89, 71, 54, 48, 49, 1, 72, 88, 25, 69))
|
||||
is.Equal(89, math.Max(89, 71, 54, 48, 49, 1, 72, 88, 25, 69))
|
||||
|
||||
is.Equal(0.9348207729, math.Max(
|
||||
0.3943310720,
|
||||
0.1090868377,
|
||||
0.9348207729,
|
||||
0.3525527584,
|
||||
0.4359833682,
|
||||
0.7958538081,
|
||||
0.1439352569,
|
||||
0.1547311967,
|
||||
0.6403818871,
|
||||
0.8618832818,
|
||||
))
|
||||
|
||||
is.Equal(0.1090868377, math.Min(
|
||||
0.3943310720,
|
||||
0.1090868377,
|
||||
0.9348207729,
|
||||
0.3525527584,
|
||||
0.4359833682,
|
||||
0.7958538081,
|
||||
0.1439352569,
|
||||
0.1547311967,
|
||||
0.6403818871,
|
||||
0.8618832818,
|
||||
))
|
||||
|
||||
}
|
||||
|
||||
func TestPagerBox(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
tests := []struct {
|
||||
first uint64
|
||||
last uint64
|
||||
pos int64
|
||||
n int64
|
||||
|
||||
start uint64
|
||||
count int64
|
||||
}{
|
||||
{1, 10, 0, 10, 1, 10},
|
||||
{1, 10, 0, 11, 1, 10},
|
||||
{1, 5, 0, 10, 1, 5},
|
||||
{1, 10, 4, 10, 5, 6},
|
||||
{1, 10, 5, 10, 6, 5},
|
||||
{1, 10, 0, -10, 0, 0},
|
||||
{1, 10, 1, -1, 2, -1},
|
||||
{1, 10, 1, -10, 2, -1},
|
||||
{1, 10, -1, 1, 10, 1},
|
||||
{1, 10, -2, 10, 9, 2},
|
||||
{1, 10, -1, -1, 10, -1},
|
||||
{1, 10, -2, -10, 9, -9},
|
||||
{1, 10, 0, -10, 0, 0},
|
||||
{1, 10, 10, 10, 0, 0},
|
||||
{1, 10, 0, ev.AllEvents, 1, 10},
|
||||
{1, 10, -1, -ev.AllEvents, 10, -10},
|
||||
|
||||
{5, 10, 0, 1, 5, 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
start, count := math.PagerBox(tt.first, tt.last, tt.pos, tt.n)
|
||||
if count > 0 {
|
||||
t.Log(tt, "|", start, count, int64(start)+count-1)
|
||||
} else {
|
||||
t.Log(tt, "|", start, count, int64(start)+count+1)
|
||||
}
|
||||
|
||||
is.Equal(start, tt.start)
|
||||
is.Equal(count, tt.count)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type mux struct {
|
||||
*http.ServeMux
|
||||
api *http.ServeMux
|
||||
wellknown *http.ServeMux
|
||||
}
|
||||
|
||||
func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) {
|
||||
for _, fn := range fns {
|
||||
// log.Printf("HTTP: %T", fn)
|
||||
fn.RegisterHTTP(mux.ServeMux)
|
||||
|
||||
if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
|
||||
// log.Printf("APIv1: %T", fn)
|
||||
fn.RegisterAPIv1(mux.api)
|
||||
}
|
||||
|
||||
if fn, ok := fn.(interface{ RegisterWellKnown(*http.ServeMux) }); ok {
|
||||
// log.Printf("WellKnown: %T", fn)
|
||||
fn.RegisterWellKnown(mux.wellknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
func New() *mux {
|
||||
mux := &mux{
|
||||
api: http.NewServeMux(),
|
||||
wellknown: http.NewServeMux(),
|
||||
ServeMux: http.NewServeMux(),
|
||||
}
|
||||
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api))
|
||||
mux.Handle("/.well-known/", http.StripPrefix("/.well-known", mux.wellknown))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
type RegisterHTTP func(*http.ServeMux)
|
||||
|
||||
func (fn RegisterHTTP) RegisterHTTP(mux *http.ServeMux) { fn(mux) }
|
||||
@@ -1,104 +0,0 @@
|
||||
package mux_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.sour.is/ev/pkg/mux"
|
||||
)
|
||||
|
||||
type mockHTTP struct {
|
||||
onServeHTTP func()
|
||||
onServeAPIv1 func()
|
||||
onServeWellKnown func()
|
||||
}
|
||||
|
||||
func (*mockHTTP) ServeFn(fn func()) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) { fn() }
|
||||
}
|
||||
func (h *mockHTTP) RegisterHTTP(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/", h.ServeFn(h.onServeHTTP))
|
||||
}
|
||||
func (h *mockHTTP) RegisterAPIv1(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/ping", h.ServeFn(h.onServeAPIv1))
|
||||
}
|
||||
func (h *mockHTTP) RegisterWellKnown(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/echo", h.ServeFn(h.onServeWellKnown))
|
||||
}
|
||||
|
||||
func TestHttp(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
called := false
|
||||
calledAPIv1 := false
|
||||
calledWellKnown := false
|
||||
|
||||
mux := mux.New()
|
||||
mux.Add(&mockHTTP{
|
||||
func() { called = true },
|
||||
func() { calledAPIv1 = true },
|
||||
func() { calledWellKnown = true },
|
||||
})
|
||||
|
||||
is.True(mux != nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
mux.ServeHTTP(w, r)
|
||||
|
||||
is.True(called)
|
||||
is.True(!calledAPIv1)
|
||||
is.True(!calledWellKnown)
|
||||
}
|
||||
|
||||
func TestHttpAPIv1(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
called := false
|
||||
calledAPIv1 := false
|
||||
calledWellKnown := false
|
||||
|
||||
mux := mux.New()
|
||||
mux.Add(&mockHTTP{
|
||||
func() { called = true },
|
||||
func() { calledAPIv1 = true },
|
||||
func() { calledWellKnown = true },
|
||||
})
|
||||
|
||||
is.True(mux != nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/ping", nil)
|
||||
mux.ServeHTTP(w, r)
|
||||
|
||||
is.True(!called)
|
||||
is.True(calledAPIv1)
|
||||
is.True(!calledWellKnown)
|
||||
}
|
||||
|
||||
func TestHttpWellKnown(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
called := false
|
||||
calledAPIv1 := false
|
||||
calledWellKnown := false
|
||||
|
||||
mux := mux.New()
|
||||
mux.Add(&mockHTTP{
|
||||
func() { called = true },
|
||||
func() { calledAPIv1 = true },
|
||||
func() { calledWellKnown = true },
|
||||
})
|
||||
|
||||
is.True(mux != nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/.well-known/echo", nil)
|
||||
mux.ServeHTTP(w, r)
|
||||
|
||||
is.True(!called)
|
||||
is.True(!calledAPIv1)
|
||||
is.True(calledWellKnown)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.sour.is/ev/internal/lg"
|
||||
"go.sour.is/ev/pkg/cron"
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type crontab interface {
|
||||
NewCron(expr string, task func(context.Context, time.Time) error)
|
||||
RunOnce(ctx context.Context, once func(context.Context, time.Time) error)
|
||||
}
|
||||
type Harness struct {
|
||||
crontab
|
||||
|
||||
Services []any
|
||||
|
||||
onStart []func(context.Context) error
|
||||
onRunning chan struct{}
|
||||
onStop []func(context.Context) error
|
||||
}
|
||||
|
||||
func (s *Harness) Setup(ctx context.Context, apps ...application) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
// setup crontab
|
||||
c := cron.New(cron.DefaultGranularity)
|
||||
s.OnStart(c.Run)
|
||||
s.onRunning = make(chan struct{})
|
||||
s.crontab = c
|
||||
|
||||
var err error
|
||||
for _, app := range apps {
|
||||
err = multierr.Append(err, app(ctx, s))
|
||||
}
|
||||
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
func (s *Harness) OnStart(fn func(context.Context) error) {
|
||||
s.onStart = append(s.onStart, fn)
|
||||
}
|
||||
func (s *Harness) OnRunning() <-chan struct{} {
|
||||
return s.onRunning
|
||||
}
|
||||
func (s *Harness) OnStop(fn func(context.Context) error) {
|
||||
s.onStop = append(s.onStop, fn)
|
||||
}
|
||||
func (s *Harness) Add(svcs ...any) {
|
||||
s.Services = append(s.Services, svcs...)
|
||||
}
|
||||
func (s *Harness) stop(ctx context.Context) error {
|
||||
g, _ := errgroup.WithContext(ctx)
|
||||
for i := range s.onStop {
|
||||
fn := s.onStop[i]
|
||||
g.Go(func() error {
|
||||
if err := fn(ctx); err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return g.Wait()
|
||||
}
|
||||
func (s *Harness) Run(ctx context.Context, appName, version string) error {
|
||||
{
|
||||
ctx, span := lg.Span(ctx)
|
||||
|
||||
log.Println(appName, version)
|
||||
span.SetAttributes(
|
||||
attribute.String("app", appName),
|
||||
attribute.String("version", version),
|
||||
)
|
||||
|
||||
Mup, err := lg.Meter(ctx).SyncInt64().UpDownCounter("up")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Mup.Add(ctx, 1)
|
||||
|
||||
span.End()
|
||||
}
|
||||
|
||||
g, _ := errgroup.WithContext(ctx)
|
||||
g.Go(func() error {
|
||||
<-ctx.Done()
|
||||
// shutdown jobs
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return s.stop(ctx)
|
||||
})
|
||||
|
||||
for i := range s.onStart {
|
||||
fn := s.onStart[i]
|
||||
g.Go(func() error { return fn(ctx) })
|
||||
}
|
||||
|
||||
close(s.onRunning)
|
||||
|
||||
err := g.Wait()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type application func(context.Context, *Harness) error // Len is the number of elements in the collection.
|
||||
|
||||
type appscore struct {
|
||||
score int
|
||||
application
|
||||
}
|
||||
type Apps []appscore
|
||||
|
||||
func (a *Apps) Apps() []application {
|
||||
sort.Sort(a)
|
||||
lis := make([]application, len(*a))
|
||||
for i, app := range *a {
|
||||
lis[i] = app.application
|
||||
}
|
||||
return lis
|
||||
}
|
||||
|
||||
// Len is the number of elements in the collection.
|
||||
func (a *Apps) Len() int {
|
||||
if a == nil {
|
||||
return 0
|
||||
}
|
||||
return len(*a)
|
||||
}
|
||||
|
||||
// Less reports whether the element with index i
|
||||
func (a *Apps) Less(i int, j int) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return (*a)[i].score < (*a)[j].score
|
||||
}
|
||||
|
||||
// Swap swaps the elements with indexes i and j.
|
||||
func (a *Apps) Swap(i int, j int) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
(*a)[i], (*a)[j] = (*a)[j], (*a)[i]
|
||||
}
|
||||
|
||||
func (a *Apps) Register(score int, app application) (none struct{}) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
*a = append(*a, appscore{score, app})
|
||||
return
|
||||
}
|
||||
|
||||
func AppName() (string, string) {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
_, name, _ := strings.Cut(info.Main.Path, "/")
|
||||
name = strings.Replace(name, "-", ".", -1)
|
||||
name = strings.Replace(name, "/", "-", -1)
|
||||
return name, info.Main.Version
|
||||
}
|
||||
|
||||
return "sour.is-app", "(devel)"
|
||||
}
|
||||
137
pkg/set/set.go
137
pkg/set/set.go
@@ -1,137 +0,0 @@
|
||||
package set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/ev/pkg/math"
|
||||
)
|
||||
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
||||
func New[T comparable](items ...T) Set[T] {
|
||||
s := make(map[T]struct{}, len(items))
|
||||
for i := range items {
|
||||
s[items[i]] = struct{}{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
func (s Set[T]) Has(v T) bool {
|
||||
_, ok := (s)[v]
|
||||
return ok
|
||||
}
|
||||
func (s Set[T]) Add(items ...T) Set[T] {
|
||||
for _, i := range items {
|
||||
s[i] = struct{}{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
func (s Set[T]) Delete(items ...T) Set[T] {
|
||||
for _, i := range items {
|
||||
delete(s, i)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s Set[T]) Equal(e Set[T]) bool {
|
||||
for k := range s {
|
||||
if _, ok := e[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for k := range e {
|
||||
if _, ok := s[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s Set[T]) String() string {
|
||||
if s == nil {
|
||||
return "set(<nil>)"
|
||||
}
|
||||
lis := make([]string, 0, len(s))
|
||||
for k := range s {
|
||||
lis = append(lis, fmt.Sprint(k))
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("set(")
|
||||
b.WriteString(strings.Join(lis, ","))
|
||||
b.WriteString(")")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type ordered interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
type BoundSet[T ordered] struct {
|
||||
min, max T
|
||||
s Set[T]
|
||||
}
|
||||
|
||||
func NewBoundSet[T ordered](min, max T, items ...T) *BoundSet[T] {
|
||||
b := &BoundSet[T]{
|
||||
min: min,
|
||||
max: max,
|
||||
s: New[T](),
|
||||
}
|
||||
b.Add(items...)
|
||||
return b
|
||||
}
|
||||
func (l *BoundSet[T]) Add(items ...T) *BoundSet[T] {
|
||||
n := 0
|
||||
for i := range items {
|
||||
if items[i] >= l.min && items[i] <= l.max {
|
||||
items[n] = items[i]
|
||||
n++
|
||||
}
|
||||
}
|
||||
l.s.Add(items[:n]...)
|
||||
return l
|
||||
}
|
||||
func (l *BoundSet[T]) AddRange(min, max T) {
|
||||
min = math.Max(min, l.min)
|
||||
max = math.Min(max, l.max)
|
||||
var lis []T
|
||||
for ; min <= max; min++ {
|
||||
lis = append(lis, min)
|
||||
}
|
||||
l.s.Add(lis...)
|
||||
}
|
||||
func (l *BoundSet[T]) Delete(items ...T) *BoundSet[T] {
|
||||
n := 0
|
||||
for i := range items {
|
||||
if items[i] >= l.min && items[i] <= l.max {
|
||||
items[n] = items[i]
|
||||
n++
|
||||
}
|
||||
}
|
||||
l.s.Delete(items[:n]...)
|
||||
return l
|
||||
}
|
||||
func (l *BoundSet[T]) Has(v T) bool {
|
||||
return l.s.Has(v)
|
||||
}
|
||||
func (l *BoundSet[T]) String() string {
|
||||
lis := make([]string, len(l.s))
|
||||
n := 0
|
||||
for k := range l.s {
|
||||
lis[n] = fmt.Sprint(k)
|
||||
n++
|
||||
}
|
||||
sort.Strings(lis)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("set(")
|
||||
b.WriteString(strings.Join(lis, ","))
|
||||
b.WriteString(")")
|
||||
return b.String()
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package set_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.sour.is/ev/pkg/set"
|
||||
)
|
||||
|
||||
func TestStringSet(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
s := set.New(strings.Fields("one two three")...)
|
||||
|
||||
is.True(s.Has("one"))
|
||||
is.True(s.Has("two"))
|
||||
is.True(s.Has("three"))
|
||||
is.True(!s.Has("four"))
|
||||
|
||||
is.Equal(set.New("one").String(), "set(one)")
|
||||
|
||||
var n set.Set[string]
|
||||
is.Equal(n.String(), "set(<nil>)")
|
||||
}
|
||||
|
||||
func TestBoundSet(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
s := set.NewBoundSet(1, 100, 1, 2, 3, 100, 1001)
|
||||
|
||||
is.True(s.Has(1))
|
||||
is.True(s.Has(2))
|
||||
is.True(s.Has(3))
|
||||
is.True(!s.Has(1001))
|
||||
|
||||
is.Equal(set.NewBoundSet(1, 100, 1).String(), "set(1)")
|
||||
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package slice
|
||||
|
||||
import (
|
||||
"go.sour.is/ev/pkg/math"
|
||||
)
|
||||
|
||||
// FilterType returns a subset that matches the type.
|
||||
func FilterType[T any](in ...any) []T {
|
||||
lis := make([]T, 0, len(in))
|
||||
for _, u := range in {
|
||||
if t, ok := u.(T); ok {
|
||||
lis = append(lis, t)
|
||||
}
|
||||
}
|
||||
return lis
|
||||
}
|
||||
|
||||
func FilterFn[T any](fn func(T) bool, in ...T) []T {
|
||||
lis := make([]T, 0, len(in))
|
||||
for _, t := range in {
|
||||
if fn(t) {
|
||||
lis = append(lis, t)
|
||||
}
|
||||
}
|
||||
return lis
|
||||
}
|
||||
|
||||
// Find returns the first of type found. or false if not found.
|
||||
func Find[T any](in ...any) (T, bool) {
|
||||
return First(FilterType[T](in...)...)
|
||||
}
|
||||
|
||||
func FindFn[T any](fn func(T) bool, in ...T) (T, bool) {
|
||||
return First(FilterFn(fn, in...)...)
|
||||
}
|
||||
|
||||
// First returns the first element in a slice.
|
||||
func First[T any](in ...T) (T, bool) {
|
||||
if len(in) == 0 {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
return in[0], true
|
||||
}
|
||||
|
||||
// Map applys func to each element s and returns results as slice.
|
||||
func Map[T, U any](f func(int, T) U) func(...T) []U {
|
||||
return func(lis ...T) []U {
|
||||
r := make([]U, len(lis))
|
||||
for i, v := range lis {
|
||||
r[i] = f(i, v)
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
func Reduce[T, R any](r R, fn func(T, R, int) R) func(...T) R {
|
||||
return func(lis ...T) R {
|
||||
for i, t := range lis {
|
||||
r = fn(t, r, i)
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
type Pair[K comparable, V any] struct {
|
||||
Key K
|
||||
Value V
|
||||
}
|
||||
|
||||
func FromMap[K comparable, V any](m map[K]V) (keys []K, values []V) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
keys = FromMapKeys(m)
|
||||
return keys, FromMapValues(m, keys)
|
||||
}
|
||||
|
||||
func FromMapKeys[K comparable, V any](m map[K]V) (keys []K) {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys = make([]K, 0, len(m))
|
||||
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func FromMapValues[K comparable, V any](m map[K]V, keys []K) (values []V) {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
values = make([]V, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
values = append(values, m[k])
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
func ToMap[K comparable, V any](keys []K, values []V) (m map[K]V) {
|
||||
m = make(map[K]V, len(keys))
|
||||
|
||||
for i := range keys {
|
||||
if len(values) < i {
|
||||
break
|
||||
}
|
||||
m[keys[i]] = values[i]
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func Zip[K comparable, V any](k []K, v []V) []Pair[K, V] {
|
||||
lis := make([]Pair[K, V], math.Max(len(k), len(v)))
|
||||
for i := range lis {
|
||||
if k != nil && len(k) > i {
|
||||
lis[i].Key = k[i]
|
||||
}
|
||||
|
||||
if v != nil && len(v) > i {
|
||||
lis[i].Value = v[i]
|
||||
}
|
||||
}
|
||||
return lis
|
||||
}
|
||||
|
||||
func Align[T any](k []T, v []T, less func(T, T) bool) []Pair[*T, *T] {
|
||||
lis := make([]Pair[*T, *T], 0, math.Max(len(k), len(v)))
|
||||
|
||||
var j int
|
||||
|
||||
for i := 0; i < len(k); {
|
||||
if j >= len(v) || less(k[i], v[j]) {
|
||||
lis = append(lis, Pair[*T, *T]{&k[i], nil})
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if less(v[j], k[i]) {
|
||||
lis = append(lis, Pair[*T, *T]{nil, &v[j]})
|
||||
j++
|
||||
continue
|
||||
}
|
||||
|
||||
lis = append(lis, Pair[*T, *T]{&k[i], &v[j]})
|
||||
i++
|
||||
j++
|
||||
}
|
||||
for ; j < len(v); j++ {
|
||||
lis = append(lis, Pair[*T, *T]{nil, &v[j]})
|
||||
}
|
||||
|
||||
return lis
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package slice_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.sour.is/ev/pkg/slice"
|
||||
)
|
||||
|
||||
func TestAlign(t *testing.T) {
|
||||
type testCase struct {
|
||||
left, right []string
|
||||
combined []slice.Pair[*string, *string]
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
left: []string{"1", "3", "5"},
|
||||
right: []string{"2", "3", "4"},
|
||||
combined: []slice.Pair[*string, *string]{
|
||||
{ptr("1"), nil},
|
||||
{nil, ptr("2")},
|
||||
{ptr("3"), ptr("3")},
|
||||
{nil, ptr("4")},
|
||||
{ptr("5"), nil},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
left: []string{"2", "3", "4"},
|
||||
right: []string{"1", "3", "5"},
|
||||
combined: []slice.Pair[*string, *string]{
|
||||
{nil, ptr("1")},
|
||||
{ptr("2"), nil},
|
||||
{ptr("3"), ptr("3")},
|
||||
{ptr("4"), nil},
|
||||
{nil, ptr("5")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
is := is.New(t)
|
||||
|
||||
for _, tt := range tests {
|
||||
combined := slice.Align(tt.left, tt.right, func(l, r string) bool { return l < r })
|
||||
is.Equal(len(combined), len(tt.combined))
|
||||
for i := range combined {
|
||||
is.Equal(combined[i], tt.combined[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
Reference in New Issue
Block a user