Compare commits
8 commits
9870d87d41
...
ccf98c2f6e
Author | SHA1 | Date | |
---|---|---|---|
ccf98c2f6e | |||
99b00887a8 | |||
695b48ae91 | |||
2b737d056c | |||
4013622afc | |||
8bb1797894 | |||
be9ed2adef | |||
09de0a19e1 |
23 changed files with 1193 additions and 310 deletions
|
@ -1,15 +1,15 @@
|
|||
package containers
|
||||
|
||||
type ChainElem[T any] struct {
|
||||
type chainElem[T any] struct {
|
||||
Elem *T
|
||||
Next *ChainElem[T]
|
||||
Next *chainElem[T]
|
||||
}
|
||||
|
||||
// reachable checks if you can reach elem l when starting from elem f.
|
||||
// It detects loops and returns false if it runs into one.
|
||||
func reachable[T any](f, l *ChainElem[T]) bool {
|
||||
func reachable[T any](f, l *chainElem[T]) bool {
|
||||
// Map to keep track of nodes already visited
|
||||
checks := make(map[*ChainElem[T]]bool)
|
||||
checks := make(map[*chainElem[T]]bool)
|
||||
for w := f; w != l; w = w.Next {
|
||||
if w == nil {
|
||||
return false
|
||||
|
@ -26,8 +26,8 @@ func reachable[T any](f, l *ChainElem[T]) bool {
|
|||
}
|
||||
|
||||
// emptyElem creates a new ChainElem[T] with empty values.
|
||||
func emptyElem[T any]() *ChainElem[T] {
|
||||
return &ChainElem[T]{
|
||||
func emptyElem[T any]() *chainElem[T] {
|
||||
return &chainElem[T]{
|
||||
Elem: nil,
|
||||
Next: nil,
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package containers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var ErrInvalidQueue = errors.New("invalid queue")
|
||||
var ErrEmptyQueue = errors.New("empty queue")
|
||||
|
||||
type Queue[T any] struct {
|
||||
head *ChainElem[T]
|
||||
tail *ChainElem[T]
|
||||
head *chainElem[T]
|
||||
tail *chainElem[T]
|
||||
}
|
||||
|
||||
// isValid checks if the queue is still valid.
|
||||
|
@ -25,7 +27,7 @@ func (q *Queue[T]) IsEmpty() bool {
|
|||
// Push adds a new element to the end of the queue.
|
||||
func (q *Queue[T]) Push(elem *T) error {
|
||||
if !q.isValid() {
|
||||
return errors.New("invalid queue")
|
||||
return ErrInvalidQueue
|
||||
}
|
||||
|
||||
e := emptyElem[T]()
|
||||
|
@ -40,10 +42,10 @@ func (q *Queue[T]) Push(elem *T) error {
|
|||
// It errors out if there is no element or the queue is invalid.
|
||||
func (q *Queue[T]) Pop() (*T, error) {
|
||||
if !q.isValid() {
|
||||
return nil, errors.New("invalid queue")
|
||||
return nil, ErrInvalidQueue
|
||||
}
|
||||
if q.IsEmpty() {
|
||||
return nil, errors.New("empty queue")
|
||||
return nil, ErrEmptyQueue
|
||||
}
|
||||
Elem := q.head.Elem
|
||||
q.head = q.head.Next
|
||||
|
@ -54,38 +56,14 @@ func (q *Queue[T]) Pop() (*T, error) {
|
|||
// It errors out if there is no element or the queue is invalid.
|
||||
func (q *Queue[T]) Top() (*T, error) {
|
||||
if !q.isValid() {
|
||||
return nil, errors.New("queue invalid")
|
||||
return nil, ErrInvalidQueue
|
||||
}
|
||||
if q.IsEmpty() {
|
||||
return nil, errors.New("queue empty")
|
||||
return nil, ErrEmptyQueue
|
||||
}
|
||||
return q.head.Elem, nil
|
||||
}
|
||||
|
||||
// HeadElem returns the first ChainElem of the queue without removing it.
|
||||
// It errors out if there is no element or the queue is invalid.
|
||||
func (q *Queue[T]) HeadElem() (*ChainElem[T], error) {
|
||||
if !q.isValid() {
|
||||
return nil, errors.New("queue invalid")
|
||||
}
|
||||
if q.IsEmpty() {
|
||||
return nil, errors.New("queue empty")
|
||||
}
|
||||
return q.head, nil
|
||||
}
|
||||
|
||||
// MarshalJSON is used for generating json data when using json.Marshal.
|
||||
func (q *Queue[T]) MarshalJSON() ([]byte, error) {
|
||||
if !q.isValid() {
|
||||
return nil, errors.New("queue invalid")
|
||||
}
|
||||
if q.IsEmpty() {
|
||||
return nil, errors.New("queue empty")
|
||||
}
|
||||
|
||||
return json.Marshal(q.head)
|
||||
}
|
||||
|
||||
func BuildQueue[T any]() *Queue[T] {
|
||||
empty := emptyElem[T]()
|
||||
return &Queue[T]{
|
||||
|
|
95
containers/queues_test.go
Normal file
95
containers/queues_test.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package containers_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/containers"
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
)
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
queue := containers.BuildQueue[int]()
|
||||
elem, err := queue.Top()
|
||||
switch err {
|
||||
case containers.ErrEmptyQueue:
|
||||
// Expected, continue
|
||||
case containers.ErrInvalidQueue:
|
||||
t.Fatal("New queue is invalid")
|
||||
case nil:
|
||||
t.Fatalf("New queue shouldn't have any elements yet: %v", elem)
|
||||
}
|
||||
elem, err = queue.Pop()
|
||||
switch err {
|
||||
case containers.ErrEmptyQueue:
|
||||
// Expected, continue
|
||||
case containers.ErrInvalidQueue:
|
||||
t.Fatal("New queue is invalid")
|
||||
case nil:
|
||||
t.Fatalf("New queue shouldn't have any elements yet: %v", elem)
|
||||
}
|
||||
if !queue.IsEmpty() {
|
||||
t.Fatal("New queue isn't empty")
|
||||
}
|
||||
|
||||
if err = queue.Push(other.IntoPointer(10)); err != nil {
|
||||
t.Fatal("Failed to push value onto new queue")
|
||||
}
|
||||
if queue.IsEmpty() {
|
||||
t.Fatal("Queue is empty after pushing a value")
|
||||
}
|
||||
|
||||
if err = queue.Push(other.IntoPointer(20)); err != nil {
|
||||
t.Fatal("Failed to push value onto queue")
|
||||
}
|
||||
|
||||
elem, err = queue.Top()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get top element from queue after inserting one: %v", err)
|
||||
}
|
||||
switch {
|
||||
case elem == nil:
|
||||
t.Fatal("Got nil elem; want &10")
|
||||
case *elem == 10:
|
||||
// Expected, continue
|
||||
default:
|
||||
t.Fatalf("Got %v; want &10", *elem)
|
||||
}
|
||||
|
||||
if queue.IsEmpty() {
|
||||
t.Fatal("Top shouldn't pop an element from the queue")
|
||||
}
|
||||
|
||||
elem, err = queue.Pop()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error while popping a value: %v", err)
|
||||
}
|
||||
switch {
|
||||
case elem == nil:
|
||||
t.Fatal("Got nil elem; want &10")
|
||||
case *elem == 10:
|
||||
// Expected, continue
|
||||
default:
|
||||
t.Fatalf("Got %v; want &10", *elem)
|
||||
}
|
||||
|
||||
if queue.IsEmpty() {
|
||||
t.Fatal("Queue should still have one element left")
|
||||
}
|
||||
|
||||
elem, err = queue.Pop()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error while popping a value: %v", err)
|
||||
}
|
||||
switch {
|
||||
case elem == nil:
|
||||
t.Fatal("Got nil elem; want &20")
|
||||
case *elem == 20:
|
||||
// Expected, continue
|
||||
default:
|
||||
t.Fatalf("Got %v; want &20", *elem)
|
||||
}
|
||||
|
||||
if !queue.IsEmpty() {
|
||||
t.Fatal("Queue should be empty now")
|
||||
}
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
package containers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Stack[T any] struct {
|
||||
top *ChainElem[T]
|
||||
bottom *ChainElem[T]
|
||||
top *chainElem[T]
|
||||
bottom *chainElem[T]
|
||||
}
|
||||
|
||||
// isValid checks if the stack is valid.
|
||||
|
@ -61,17 +60,6 @@ func (s *Stack[T]) Top() (*T, error) {
|
|||
return s.top.Elem, nil
|
||||
}
|
||||
|
||||
// MarshalJSON is used by json.Marshal to create a json representation.
|
||||
func (s *Stack[T]) MarshalJSON() ([]byte, error) {
|
||||
if !s.isValid() {
|
||||
return nil, errors.New("queue invalid")
|
||||
}
|
||||
if s.IsEmpty() {
|
||||
return nil, errors.New("queue empty")
|
||||
}
|
||||
|
||||
return json.Marshal(s.top)
|
||||
}
|
||||
func BuildStack[T any]() *Stack[T] {
|
||||
empty := emptyElem[T]()
|
||||
return &Stack[T]{
|
||||
|
|
108
containers/stacks_test.go
Normal file
108
containers/stacks_test.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package containers_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/containers"
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
)
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
stack := containers.BuildStack[int]()
|
||||
elem, err := stack.Top()
|
||||
switch err {
|
||||
case containers.ErrEmptyQueue:
|
||||
// Expected, continue
|
||||
case containers.ErrInvalidQueue:
|
||||
t.Fatal("New queue is invalid")
|
||||
case nil:
|
||||
t.Fatalf("New queue shouldn't have any elements yet: %v", elem)
|
||||
}
|
||||
elem, err = stack.Pop()
|
||||
switch err {
|
||||
case containers.ErrEmptyQueue:
|
||||
// Expected, continue
|
||||
case containers.ErrInvalidQueue:
|
||||
t.Fatal("New queue is invalid")
|
||||
case nil:
|
||||
t.Fatalf("New queue shouldn't have any elements yet: %v", elem)
|
||||
}
|
||||
if !stack.IsEmpty() {
|
||||
t.Fatal("New queue isn't empty")
|
||||
}
|
||||
|
||||
if err = stack.Push(other.IntoPointer(10)); err != nil {
|
||||
t.Fatal("Failed to push value onto new queue")
|
||||
}
|
||||
if stack.IsEmpty() {
|
||||
t.Fatal("Queue is empty after pushing a value")
|
||||
}
|
||||
|
||||
elem, err = stack.Top()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get top element from queue after inserting one: %v", err)
|
||||
}
|
||||
switch {
|
||||
case elem == nil:
|
||||
t.Fatal("Got nil elem; want &10")
|
||||
case *elem == 10:
|
||||
// Expected, continue
|
||||
default:
|
||||
t.Fatalf("Got %v; want &10", *elem)
|
||||
}
|
||||
|
||||
if err = stack.Push(other.IntoPointer(20)); err != nil {
|
||||
t.Fatal("Failed to push value onto queue")
|
||||
}
|
||||
|
||||
elem, err = stack.Top()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get top element from queue after inserting one: %v", err)
|
||||
}
|
||||
switch {
|
||||
case elem == nil:
|
||||
t.Fatal("Got nil elem; want &20")
|
||||
case *elem == 20:
|
||||
// Expected, continue
|
||||
default:
|
||||
t.Fatalf("Got %v; want &20", *elem)
|
||||
}
|
||||
|
||||
if stack.IsEmpty() {
|
||||
t.Fatal("Top shouldn't pop an element from the queue")
|
||||
}
|
||||
|
||||
elem, err = stack.Pop()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error while popping a value: %v", err)
|
||||
}
|
||||
switch {
|
||||
case elem == nil:
|
||||
t.Fatal("Got nil elem; want &20")
|
||||
case *elem == 20:
|
||||
// Expected, continue
|
||||
default:
|
||||
t.Fatalf("Got %v; want &20", *elem)
|
||||
}
|
||||
|
||||
if stack.IsEmpty() {
|
||||
t.Fatal("Queue should still have one element left")
|
||||
}
|
||||
|
||||
elem, err = stack.Pop()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error while popping a value: %v", err)
|
||||
}
|
||||
switch {
|
||||
case elem == nil:
|
||||
t.Fatal("Got nil elem; want &10")
|
||||
case *elem == 10:
|
||||
// Expected, continue
|
||||
default:
|
||||
t.Fatalf("Got %v; want &10", *elem)
|
||||
}
|
||||
|
||||
if !stack.IsEmpty() {
|
||||
t.Fatal("Queue should be empty now")
|
||||
}
|
||||
}
|
BIN
coverage_badge.png
Normal file
BIN
coverage_badge.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
27
embedFsWrapper/wrapper_test.go
Normal file
27
embedFsWrapper/wrapper_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package embedFsWrapper_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/embedFsWrapper"
|
||||
)
|
||||
|
||||
func TestFSWrapper(t *testing.T) {
|
||||
mfs := fstest.MapFS{
|
||||
"baz/foo": &fstest.MapFile{Data: []byte("bar")},
|
||||
}
|
||||
wrapped := embedFsWrapper.NewFSWrapper(mfs, "baz/")
|
||||
f, err := wrapped.Open("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to open file foo (baz/foo), got %v", err)
|
||||
}
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "bar" {
|
||||
t.Fatalf("Expected file to have \"bar\" as content, found %v", string(data))
|
||||
}
|
||||
}
|
33
http/chain_test.go
Normal file
33
http/chain_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package webutils_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
)
|
||||
|
||||
func TestChainMiddlewares(t *testing.T) {
|
||||
wrapper1 := webutils.HandlerBuilder(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Add("X-Foo", "bar")
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
wrapper2 := webutils.HandlerBuilder(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Add("X-Foos", r.Header.Get("X-Foo")+"baz")
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
baseHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if x := r.Header.Get("X-Foos"); x != "barbaz" {
|
||||
t.Fatalf(`Expected header "X-Foos" to be "barbaz", got %v`, x)
|
||||
}
|
||||
})
|
||||
handler := webutils.ChainMiddlewares(baseHandler, wrapper1, wrapper2)
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
handler.ServeHTTP(recorder, req)
|
||||
}
|
23
http/context_test.go
Normal file
23
http/context_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package webutils_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
)
|
||||
|
||||
func TestContextValsMiddleware(t *testing.T) {
|
||||
builder := webutils.ContextValsMiddleware(map[any]any{"foo": "bar"})
|
||||
baseHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if val, _ := r.Context().Value("foo").(string); val != "bar" {
|
||||
t.Fatalf(`Expected context value "foo" to be "bar", got %v`, val)
|
||||
}
|
||||
})
|
||||
wrapped := builder(baseHandler)
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
wrapped.ServeHTTP(recorder, req)
|
||||
// At this point baseHandler should have been called and checked for the context value
|
||||
}
|
|
@ -12,10 +12,11 @@ import (
|
|||
// and a field "message" set to the `message`
|
||||
//
|
||||
// Deprecated: Use ProblemDetails or ProblemDetailsStatusOnly instead
|
||||
func HttpErr(w http.ResponseWriter, errId int, message string, code int) {
|
||||
func HttpErr(w http.ResponseWriter, errId int, message string, code int) error {
|
||||
w.WriteHeader(code)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, "{\"id\": %d, \"message\": \"%s\"}", errId, message)
|
||||
_, err := fmt.Fprintf(w, "{\"id\": %d, \"message\": \"%s\"}", errId, message)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write an RFC 9457 compliant problem details response
|
||||
|
@ -31,7 +32,7 @@ func ProblemDetails(
|
|||
errorTitle string,
|
||||
details *string,
|
||||
extras map[string]any,
|
||||
) {
|
||||
) error {
|
||||
w.Header().Add("Content-Type", "application/problem+json")
|
||||
w.WriteHeader(statusCode)
|
||||
data := map[string]any{
|
||||
|
@ -42,22 +43,20 @@ func ProblemDetails(
|
|||
if details != nil {
|
||||
data["detail"] = *details
|
||||
}
|
||||
if extras != nil {
|
||||
for k, v := range extras {
|
||||
if _, ok := data[k]; ok {
|
||||
// Don't overwrite default fields
|
||||
continue
|
||||
}
|
||||
data[k] = v
|
||||
for k, v := range extras {
|
||||
if _, ok := data[k]; ok {
|
||||
// Don't overwrite default fields
|
||||
continue
|
||||
}
|
||||
data[k] = v
|
||||
}
|
||||
enc := json.NewEncoder(w)
|
||||
enc.Encode(data)
|
||||
return enc.Encode(data)
|
||||
}
|
||||
|
||||
// Write a simple problem details response.
|
||||
// It only provides the status code, as defined in RFC 9457, section 4.2.1
|
||||
func ProblemDetailsStatusOnly(w http.ResponseWriter, statusCode int) {
|
||||
func ProblemDetailsStatusOnly(w http.ResponseWriter, statusCode int) error {
|
||||
w.Header().Add("Content-Type", "application/problem+json")
|
||||
w.WriteHeader(statusCode)
|
||||
data := map[string]any{
|
||||
|
@ -68,5 +67,5 @@ func ProblemDetailsStatusOnly(w http.ResponseWriter, statusCode int) {
|
|||
}
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.Encode(data)
|
||||
return enc.Encode(data)
|
||||
}
|
||||
|
|
235
http/httpErr_test.go
Normal file
235
http/httpErr_test.go
Normal file
|
@ -0,0 +1,235 @@
|
|||
package webutils_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
"git.mstar.dev/mstar/goutils/maputils"
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
)
|
||||
|
||||
func TestHttpErr(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.HttpErr(recorder, 1, "some error", 500)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
res := recorder.Result()
|
||||
if res.StatusCode != 500 {
|
||||
t.Fatalf("Expected status code 500, got %v", res.StatusCode)
|
||||
}
|
||||
body := string(other.Must(io.ReadAll(res.Body)))
|
||||
expected := `{"id": 1, "message": "some error"}`
|
||||
if body != expected {
|
||||
t.Fatalf("Expected body %q, got %q", expected, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProblemDetailsStatusOnly(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.ProblemDetailsStatusOnly(recorder, 500)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
res := recorder.Result()
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/problem+json" {
|
||||
t.Fatalf("Expected content type to be \"application/problem+json\" got %v", ct)
|
||||
}
|
||||
body := string(other.Must(io.ReadAll(res.Body)))
|
||||
data := map[string]any{}
|
||||
err = json.Unmarshal([]byte(body), &data)
|
||||
if err != nil {
|
||||
t.Fatalf("response body is not json: %v", err)
|
||||
}
|
||||
if dt, ok := data["type"].(string); !ok || dt != "about:blank" {
|
||||
t.Fatalf(`Expected key "type" as "about:blank", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["title"].(string); !ok || dt != http.StatusText(500) {
|
||||
t.Fatalf(`Expected key "title" as "%v", got %v`, http.StatusText(500), dt)
|
||||
}
|
||||
if dt, ok := data["status"].(float64); !ok || dt != 500 {
|
||||
t.Fatalf(`Expected key "status" as 500, got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["reference"].(string); !ok || dt != "RFC 9457" {
|
||||
t.Fatalf(`Expected key "reference" as "RFC 9457", got %v`, dt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProblemDetailsNoDetailsOrExtras(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.ProblemDetails(
|
||||
recorder,
|
||||
500,
|
||||
"error-type",
|
||||
"error-title",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
res := recorder.Result()
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/problem+json" {
|
||||
t.Fatalf("Expected content type to be \"application/problem+json\" got %v", ct)
|
||||
}
|
||||
body := string(other.Must(io.ReadAll(res.Body)))
|
||||
data := map[string]any{}
|
||||
err = json.Unmarshal([]byte(body), &data)
|
||||
if err != nil {
|
||||
t.Fatalf("response body is not json: %v", err)
|
||||
}
|
||||
|
||||
expectedFields := []string{"type", "title", "status"}
|
||||
gotFields := maputils.KeysFromMap(data)
|
||||
if !sliceutils.CompareUnordered(gotFields, expectedFields) {
|
||||
t.Fatalf("Expected fields %v, got %v", expectedFields, gotFields)
|
||||
}
|
||||
if dt, ok := data["type"].(string); !ok || dt != "error-type" {
|
||||
t.Fatalf(`Expected key "type" as "error-type", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["title"].(string); !ok || dt != "error-title" {
|
||||
t.Fatalf(`Expected key "title" as "error-title", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["status"].(float64); !ok || dt != 500 {
|
||||
t.Fatalf(`Expected key "status" as 500, got %v`, dt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProblemDetailsNoDetailsWithExtras(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.ProblemDetails(
|
||||
recorder,
|
||||
500,
|
||||
"error-type",
|
||||
"error-title",
|
||||
nil,
|
||||
map[string]any{"foo": "bar"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
res := recorder.Result()
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/problem+json" {
|
||||
t.Fatalf("Expected content type to be \"application/problem+json\" got %v", ct)
|
||||
}
|
||||
body := string(other.Must(io.ReadAll(res.Body)))
|
||||
data := map[string]any{}
|
||||
err = json.Unmarshal([]byte(body), &data)
|
||||
if err != nil {
|
||||
t.Fatalf("response body is not json: %v", err)
|
||||
}
|
||||
|
||||
expectedFields := []string{"type", "title", "status", "foo"}
|
||||
gotFields := maputils.KeysFromMap(data)
|
||||
if !sliceutils.CompareUnordered(gotFields, expectedFields) {
|
||||
t.Fatalf("Expected fields %v, got %v", expectedFields, gotFields)
|
||||
}
|
||||
if dt, ok := data["type"].(string); !ok || dt != "error-type" {
|
||||
t.Fatalf(`Expected key "type" as "error-type", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["title"].(string); !ok || dt != "error-title" {
|
||||
t.Fatalf(`Expected key "title" as "error-title", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["status"].(float64); !ok || dt != 500 {
|
||||
t.Fatalf(`Expected key "status" as 500, got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["foo"].(string); !ok || dt != "bar" {
|
||||
t.Fatalf(`Expected key "foo" as "bar", got %v`, dt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProblemDetailsWithDetailsNoExtras(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.ProblemDetails(
|
||||
recorder,
|
||||
500,
|
||||
"error-type",
|
||||
"error-title",
|
||||
other.IntoPointer("error-details"),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
res := recorder.Result()
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/problem+json" {
|
||||
t.Fatalf("Expected content type to be \"application/problem+json\" got %v", ct)
|
||||
}
|
||||
body := string(other.Must(io.ReadAll(res.Body)))
|
||||
data := map[string]any{}
|
||||
err = json.Unmarshal([]byte(body), &data)
|
||||
if err != nil {
|
||||
t.Fatalf("response body is not json: %v", err)
|
||||
}
|
||||
|
||||
expectedFields := []string{"type", "title", "status", "detail"}
|
||||
gotFields := maputils.KeysFromMap(data)
|
||||
if !sliceutils.CompareUnordered(gotFields, expectedFields) {
|
||||
t.Fatalf("Expected fields %v, got %v", expectedFields, gotFields)
|
||||
}
|
||||
if dt, ok := data["type"].(string); !ok || dt != "error-type" {
|
||||
t.Fatalf(`Expected key "type" as "error-type", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["title"].(string); !ok || dt != "error-title" {
|
||||
t.Fatalf(`Expected key "title" as "error-title", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["status"].(float64); !ok || dt != 500 {
|
||||
t.Fatalf(`Expected key "status" as 500, got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["detail"].(string); !ok || dt != "error-details" {
|
||||
t.Fatalf(`Expected key "detail" as "error-details", got %v`, dt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProblemDetailsWithDetailsWithExtras(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.ProblemDetails(
|
||||
recorder,
|
||||
500,
|
||||
"error-type",
|
||||
"error-title",
|
||||
other.IntoPointer("error-details"),
|
||||
map[string]any{"foo": "bar", "type": "should not be set in the end"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
res := recorder.Result()
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/problem+json" {
|
||||
t.Fatalf("Expected content type to be \"application/problem+json\" got %v", ct)
|
||||
}
|
||||
body := string(other.Must(io.ReadAll(res.Body)))
|
||||
data := map[string]any{}
|
||||
err = json.Unmarshal([]byte(body), &data)
|
||||
if err != nil {
|
||||
t.Fatalf("response body is not json: %v", err)
|
||||
}
|
||||
|
||||
expectedFields := []string{"type", "title", "status", "detail", "foo"}
|
||||
gotFields := maputils.KeysFromMap(data)
|
||||
if !sliceutils.CompareUnordered(gotFields, expectedFields) {
|
||||
t.Fatalf("Expected fields %v, got %v", expectedFields, gotFields)
|
||||
}
|
||||
if dt, ok := data["type"].(string); !ok || dt != "error-type" {
|
||||
t.Fatalf(`Expected key "type" as "error-type", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["title"].(string); !ok || dt != "error-title" {
|
||||
t.Fatalf(`Expected key "title" as "error-title", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["status"].(float64); !ok || dt != 500 {
|
||||
t.Fatalf(`Expected key "status" as 500, got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["detail"].(string); !ok || dt != "error-details" {
|
||||
t.Fatalf(`Expected key "detail" as "error-details", got %v`, dt)
|
||||
}
|
||||
if dt, ok := data["foo"].(string); !ok || dt != "bar" {
|
||||
t.Fatalf(`Expected key "foo" as "bar", got %v`, dt)
|
||||
}
|
||||
}
|
|
@ -4,14 +4,17 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
)
|
||||
|
||||
func SendJson(w http.ResponseWriter, data any) error {
|
||||
encoded, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
return other.Error("webutils.SendJson", "failed to marshal data to json", err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
fmt.Fprint(w, string(encoded))
|
||||
// Can't really catch here
|
||||
_, _ = fmt.Fprint(w, string(encoded))
|
||||
return nil
|
||||
}
|
||||
|
|
45
http/json_test.go
Normal file
45
http/json_test.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package webutils_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
webutils "git.mstar.dev/mstar/goutils/http"
|
||||
)
|
||||
|
||||
func TestSendJsonOk(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.SendJson(recorder, map[string]any{
|
||||
"a": 1,
|
||||
"b": "b",
|
||||
"c": nil,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error while sending json: %v", err)
|
||||
}
|
||||
res := recorder.Result()
|
||||
if ctype := res.Header.Get("Content-Type"); ctype != "application/json" {
|
||||
t.Fatalf("expected content type \"application/json\", got %v", ctype)
|
||||
}
|
||||
// httptest guarantees body to be functional and only returned error will be EOL
|
||||
// so no need to watch for error
|
||||
bodyBytes, _ := io.ReadAll(res.Body)
|
||||
body := string(bodyBytes)
|
||||
expected := `{"a":1,"b":"b","c":null}`
|
||||
if body != expected {
|
||||
t.Fatalf("Expected %v as body, got %v", expected, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendJsonNotMarshallable(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
err := webutils.SendJson(recorder, map[string]any{
|
||||
"a": 1,
|
||||
"b": "b",
|
||||
"c": make(chan any),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected json marshalling error")
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
// File wrapper for rotating log files
|
||||
// Copied from https://stackoverflow.com/a/28797984
|
||||
package logrotate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RotateWriter struct {
|
||||
lock sync.Mutex
|
||||
filename string // should be set to the actual filename
|
||||
fp *os.File
|
||||
}
|
||||
|
||||
// Make a new RotateWriter. Return nil if error occurs during setup.
|
||||
func New(filename string) (*RotateWriter, error) {
|
||||
w := &RotateWriter{filename: filename}
|
||||
err := w.Rotate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Write satisfies the io.Writer interface.
|
||||
func (w *RotateWriter) Write(output []byte) (int, error) {
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
return w.fp.Write(output)
|
||||
}
|
||||
|
||||
// Perform the actual act of rotating and reopening file.
|
||||
func (w *RotateWriter) Rotate() (err error) {
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
|
||||
// Close existing file if open
|
||||
if w.fp != nil {
|
||||
err = w.fp.Close()
|
||||
w.fp = nil
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Rename dest file if it already exists
|
||||
_, err = os.Stat(w.filename)
|
||||
if err == nil {
|
||||
err = os.Rename(w.filename, w.filename+"."+time.Now().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create a file.
|
||||
dir := path.Dir(w.filename)
|
||||
_, err = os.Stat(dir)
|
||||
if err != nil {
|
||||
if err = os.Mkdir(dir, os.ModeDir); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
w.fp, err = os.Create(w.filename)
|
||||
return
|
||||
}
|
|
@ -65,7 +65,7 @@ func CompareMap[K, V comparable](a, b map[K]V) bool {
|
|||
// Then compare key-value pairs
|
||||
for k, v := range a {
|
||||
val, ok := b[k]
|
||||
if !(ok && val == v) {
|
||||
if !ok || val != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
274
maputils/mapUtils_test.go
Normal file
274
maputils/mapUtils_test.go
Normal file
|
@ -0,0 +1,274 @@
|
|||
package maputils_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/maputils"
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
)
|
||||
|
||||
func TestMapSameKeys(t *testing.T) {
|
||||
type args struct {
|
||||
dic map[string]int
|
||||
apply func(string, int) int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[string]int
|
||||
}{
|
||||
{
|
||||
name: "map",
|
||||
args: args{
|
||||
dic: map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
apply: func(s string, i int) int { return i + 1 },
|
||||
},
|
||||
want: map[string]int{
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"c": 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := maputils.MapSameKeys(tt.args.dic, tt.args.apply); !reflect.DeepEqual(
|
||||
got,
|
||||
tt.want,
|
||||
) {
|
||||
t.Errorf("MapSameKeys() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapNewKeys(t *testing.T) {
|
||||
type args struct {
|
||||
in map[string]int
|
||||
apply func(string, int) (string, int)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[string]int
|
||||
}{
|
||||
{
|
||||
name: "new keys",
|
||||
args: args{
|
||||
in: map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
apply: func(s string, i int) (string, int) { return s + "-", i + 1 },
|
||||
},
|
||||
want: map[string]int{
|
||||
"a-": 2,
|
||||
"b-": 3,
|
||||
"c-": 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := maputils.MapNewKeys(tt.args.in, tt.args.apply); !reflect.DeepEqual(
|
||||
got,
|
||||
tt.want,
|
||||
) {
|
||||
t.Errorf("MapNewKeys() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMap(t *testing.T) {
|
||||
type args struct {
|
||||
dic map[string]int
|
||||
filter func(string, int) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[string]int
|
||||
}{
|
||||
{
|
||||
name: "filter",
|
||||
args: args{
|
||||
dic: map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
filter: func(s string, i int) bool { return s == "a" },
|
||||
},
|
||||
want: map[string]int{"a": 1},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := maputils.FilterMap(tt.args.dic, tt.args.filter); !reflect.DeepEqual(
|
||||
got,
|
||||
tt.want,
|
||||
) {
|
||||
t.Errorf("FilterMap() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeysFromMap(t *testing.T) {
|
||||
type args struct {
|
||||
m map[string]int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "keys",
|
||||
args: args{
|
||||
m: map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := maputils.KeysFromMap(tt.args.m); !sliceutils.CompareUnordered(got, tt.want) {
|
||||
t.Errorf("KeysFromMap() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareMap(t *testing.T) {
|
||||
type args struct {
|
||||
a map[string]int
|
||||
b map[string]int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal",
|
||||
args: args{
|
||||
a: map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
b: map[string]int{
|
||||
"b": 2,
|
||||
"a": 1,
|
||||
"c": 3,
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal, same len, same keys",
|
||||
args: args{
|
||||
a: map[string]int{
|
||||
"a": 5,
|
||||
"b": 6,
|
||||
"c": 7,
|
||||
},
|
||||
b: map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not equal, diff len",
|
||||
args: args{
|
||||
a: map[string]int{
|
||||
"a": 5,
|
||||
"b": 6,
|
||||
"c": 7,
|
||||
},
|
||||
b: map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not equal, same len, diff keys",
|
||||
args: args{
|
||||
a: map[string]int{
|
||||
"a": 5,
|
||||
"b": 6,
|
||||
"c": 7,
|
||||
},
|
||||
b: map[string]int{
|
||||
"e": 1,
|
||||
"f": 2,
|
||||
"g": 3,
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := maputils.CompareMap(tt.args.a, tt.args.b); got != tt.want {
|
||||
t.Errorf("CompareMap() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompact(t *testing.T) {
|
||||
type args struct {
|
||||
m map[int]int
|
||||
compactor func(accK int, accV int, nextK int, nextV int) (int, int)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
want1 int
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{
|
||||
name: "compact",
|
||||
args: args{
|
||||
m: map[int]int{
|
||||
4: 1,
|
||||
5: 2,
|
||||
6: 3,
|
||||
},
|
||||
compactor: func(accK int, accV int, nextK int, nextV int) (int, int) { return accK + nextK, accV + nextV },
|
||||
},
|
||||
want: 4 + 5 + 6,
|
||||
want1: 1 + 2 + 3,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := maputils.Compact(tt.args.m, tt.args.compactor)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Compact() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if !reflect.DeepEqual(got1, tt.want1) {
|
||||
t.Errorf("Compact() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
16
math/math_test.go
Normal file
16
math/math_test.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package mathutils_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
mathutils "git.mstar.dev/mstar/goutils/math"
|
||||
)
|
||||
|
||||
func TestAbs(t *testing.T) {
|
||||
if x := mathutils.Abs(-5); x != 5 {
|
||||
t.Fatalf("Received %v, wanted 5", x)
|
||||
}
|
||||
if x := mathutils.Abs(6); x != 6 {
|
||||
t.Fatalf("Received %v, wanted 6", x)
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// Copyright (c) 2024 mStar
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package multiplexer
|
||||
|
||||
import "errors"
|
||||
|
||||
// A many to one multiplexer
|
||||
// Yes, channels technically already are that, but there are a bunch of problems with using raw channels as multiplexer:
|
||||
// If any of the senders tries to send to a closed channel, it explodes
|
||||
// Thus, wrap it inside a struct that handles that case of a closed channel
|
||||
type ManyToOne[T any] struct {
|
||||
outbound chan T
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewManyToOne creates a new ManyToOne multiplexer
|
||||
// The given channel will be where all messages will be sent to
|
||||
func NewManyToOne[T any](receiver chan T) ManyToOne[T] {
|
||||
return ManyToOne[T]{
|
||||
outbound: receiver,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message to this many to one plexer
|
||||
// If closed, the message won't get sent
|
||||
func (m *ManyToOne[T]) Send(msg T) error {
|
||||
if m.closed {
|
||||
return errors.New("multiplexer has been closed")
|
||||
}
|
||||
m.outbound <- msg
|
||||
return nil
|
||||
}
|
||||
|
||||
// Closes the channel and marks the plexer as closed
|
||||
func (m *ManyToOne[T]) Close() {
|
||||
close(m.outbound)
|
||||
m.closed = true
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
// Copyright (c) 2024 mStar
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package multiplexer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type OneToMany[T any] struct {
|
||||
inbound chan T
|
||||
outbound map[string]chan T // Use map here to give names to outbound channels
|
||||
lock sync.Mutex
|
||||
closeChan chan any
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewOneToMany[T any]() OneToMany[T] {
|
||||
return OneToMany[T]{
|
||||
inbound: make(chan T),
|
||||
outbound: make(map[string]chan T),
|
||||
lock: sync.Mutex{},
|
||||
closeChan: make(chan any),
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Get the channel to send things into
|
||||
func (o *OneToMany[T]) GetSender() chan T {
|
||||
return o.inbound
|
||||
}
|
||||
|
||||
// Create a new receiver for the multiplexer to send messages to.
|
||||
// Please do not close this manually, instead use the CloseReceiver func
|
||||
func (o *OneToMany[T]) MakeReceiver(name string) (chan T, error) {
|
||||
if o.closed {
|
||||
return nil, errors.New("multiplexer has been closed")
|
||||
}
|
||||
rec := make(chan T)
|
||||
|
||||
// Only allow new receivers to be made
|
||||
o.lock.Lock()
|
||||
if _, ok := o.outbound[name]; ok {
|
||||
return nil, errors.New("receiver with that name already exists")
|
||||
}
|
||||
o.lock.Unlock()
|
||||
|
||||
o.outbound[name] = rec
|
||||
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
// Closes a receiver channel with the given name and removes it from the multiplexer
|
||||
func (o *OneToMany[T]) CloseReceiver(name string) {
|
||||
if o.closed {
|
||||
return
|
||||
}
|
||||
o.lock.Lock()
|
||||
if val, ok := o.outbound[name]; ok {
|
||||
close(val)
|
||||
delete(o.outbound, name)
|
||||
}
|
||||
o.lock.Unlock()
|
||||
}
|
||||
|
||||
// Start this one to many multiplexer
|
||||
// intended to run as a goroutine (`go plexer.StartPlexer()`)
|
||||
func (o *OneToMany[T]) StartPlexer() {
|
||||
select {
|
||||
// Message gotten from inbound channel
|
||||
case msg := <-o.inbound:
|
||||
o.lock.Lock()
|
||||
// Send it to all outbound channels
|
||||
for _, c := range o.outbound {
|
||||
c <- msg
|
||||
}
|
||||
o.lock.Unlock()
|
||||
// Told to close the plexer including sender
|
||||
case <-o.closeChan:
|
||||
o.lock.Lock()
|
||||
// First close all outbound channels
|
||||
// No need to send any signal there as readers will just stop
|
||||
for _, c := range o.outbound {
|
||||
close(c)
|
||||
}
|
||||
// Then close inbound, set closed and call it a day
|
||||
close(o.inbound)
|
||||
o.closed = true
|
||||
o.lock.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Close the sender and all receiver channels, mark the plexer as closed and stop the distribution goroutine (all by sending one signal)
|
||||
func (o *OneToMany[T]) CloseSender() {
|
||||
o.closeChan <- 1
|
||||
}
|
55
other/other_test.go
Normal file
55
other/other_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package other_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
)
|
||||
|
||||
func TestOutputIntoChannel(t *testing.T) {
|
||||
c := make(chan int)
|
||||
go func() {
|
||||
other.OutputIntoChannel(10, c)
|
||||
}()
|
||||
x := <-c
|
||||
if x != 10 {
|
||||
t.Fatalf("sent %v, expected 10", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMust_mustPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("Must didn't panic on error")
|
||||
}
|
||||
}()
|
||||
_ = other.Must(10, errors.New("some error"))
|
||||
}
|
||||
|
||||
func TestMust_noPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("Must panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
x := other.Must(10, nil)
|
||||
if x != 10 {
|
||||
t.Fatalf("got %v, expected 10", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntoPointer(t *testing.T) {
|
||||
p := other.IntoPointer(10)
|
||||
if *p != 10 {
|
||||
t.Fatalf("other.IntoPointer(10) = %v, want &10", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
e := errors.New("some error")
|
||||
we := other.Error("source", "message", e)
|
||||
if we.Error() != "source: message: some error" {
|
||||
t.Fatalf("Got %v, expected \"source: message: some error\"", we)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
package other
|
||||
|
||||
// Can't really test zerolog setup functions as they rely on
|
||||
// CLI flags
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
|
@ -72,5 +75,4 @@ func configOutputs(logWriter io.Writer) {
|
|||
append([]io.Writer{log.Logger}, extraLogWriters...)...,
|
||||
)).With().Timestamp().Logger()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
243
sliceutils/sliceUtils_test.go
Normal file
243
sliceutils/sliceUtils_test.go
Normal file
|
@ -0,0 +1,243 @@
|
|||
package sliceutils_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
in := []int8{1, 2, 3, 4}
|
||||
out := []int16{2, 4, 6, 8}
|
||||
got := sliceutils.Map(in, func(t int8) int16 { return int16(t) * 2 })
|
||||
if !reflect.DeepEqual(out, got) {
|
||||
t.Fatalf("Map() = %v, want %v", got, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
in := []int8{1, 2, 3, 4}
|
||||
out := []int8{2, 4}
|
||||
got := sliceutils.Filter(in, func(t int8) bool { return t%2 == 0 })
|
||||
if !reflect.DeepEqual(out, got) {
|
||||
t.Fatalf("Map() = %v, want %v", got, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicate(t *testing.T) {
|
||||
in := []int8{1, 2, 2, 3, 3, 3, 4}
|
||||
out := []int8{1, 2, 3, 4}
|
||||
got := sliceutils.RemoveDuplicate(in)
|
||||
if !reflect.DeepEqual(out, got) {
|
||||
t.Fatalf("Map() = %v, want %v", got, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
in := []int8{1, 2, 3, 4}
|
||||
out := []int8{4, 3, 2, 1}
|
||||
sliceutils.Reverse(in)
|
||||
if !reflect.DeepEqual(out, in) {
|
||||
t.Fatalf("Map() = %v, want %v", in, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareOrdered(t *testing.T) {
|
||||
type args struct {
|
||||
a []int
|
||||
b []int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: []int{1, 2, 3, 4},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal, same len",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: []int{0, 1, 2, 3},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not equal, diff len",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: []int{0, 1, 2},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sliceutils.CompareOrdered(tt.args.a, tt.args.b); got != tt.want {
|
||||
t.Errorf("CompareOrdered() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareUnordered(t *testing.T) {
|
||||
type args struct {
|
||||
a []int
|
||||
b []int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal ordered",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: []int{1, 2, 3, 4},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal unordered",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: []int{2, 4, 3, 1},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal, same len",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: []int{0, 2, 3, 4},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not equal, diff len",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: []int{0, 1, 2},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sliceutils.CompareUnordered(tt.args.a, tt.args.b); got != tt.want {
|
||||
t.Errorf("CompareUnordered() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
type args struct {
|
||||
a []int
|
||||
b int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "contains",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: 3,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "doesnt contain",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
b: 0,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sliceutils.Contains(tt.args.a, tt.args.b); got != tt.want {
|
||||
t.Errorf("Contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsFunc(t *testing.T) {
|
||||
type args struct {
|
||||
a []int
|
||||
f func(t int) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "contains",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
f: func(t int) bool { return t == 2 },
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "doesnt contain",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
f: func(t int) bool { return t == 9 },
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sliceutils.ContainsFunc(tt.args.a, tt.args.f); got != tt.want {
|
||||
t.Errorf("ContainsFunc() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompact(t *testing.T) {
|
||||
type args struct {
|
||||
a []int
|
||||
compactor func(acc int, next int) int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "sum",
|
||||
args: args{
|
||||
a: []int{1, 2, 3, 4},
|
||||
compactor: func(acc, next int) int { return acc + next },
|
||||
},
|
||||
want: 1 + 2 + 3 + 4,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sliceutils.Compact(tt.args.a, tt.args.compactor); !reflect.DeepEqual(
|
||||
got,
|
||||
tt.want,
|
||||
) {
|
||||
t.Errorf("Compact() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package wrappedmutex
|
||||
|
||||
import "sync"
|
||||
|
||||
type Mutex[T any] struct {
|
||||
wrapped T
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func New[T any](wrapped T) Mutex[T] {
|
||||
return Mutex[T]{
|
||||
wrapped: wrapped,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mutex[T]) Lock() {
|
||||
m.lock.Lock()
|
||||
}
|
||||
|
||||
func (m *Mutex[T]) TryLock() bool {
|
||||
return m.lock.TryLock()
|
||||
}
|
||||
|
||||
func (m *Mutex[T]) Unlock() {
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
func (m *Mutex[T]) Get() *T {
|
||||
return &m.wrapped
|
||||
}
|
Loading…
Reference in a new issue