Compare commits

..

24 commits

Author SHA1 Message Date
d303f551f3
Improve channel type hint 2025-06-17 13:37:05 +02:00
7a09569c03
Docs and sliceutils from and to channel 2025-06-17 13:28:25 +02:00
3bb1984f1c
Actually make SseWriter be spec compliant
- Change inbound data type from byte slice to dedicated event message
  type
- Include data validation for id and event type
- Better documentation
2025-06-10 13:33:17 +02:00
51b35036ab
Merge branch 'main' of git.mstar.dev:mstar/goutils 2025-06-06 17:08:03 +02:00
7881a55436
Add Server Side Events helper 2025-06-06 17:07:31 +02:00
4eb5d68fbf
Fix for the fix
I didn't actually write the found ip to the logging stack (or whatever
zerolog uses). This is now fixed
2025-05-12 15:35:45 +02:00
d7ed5b1eae
fix(webutils): Bad Ip logging
Fix the logging middleware only logging the last proxy's ip instead of
the ip of the actual request
2025-05-12 15:08:53 +02:00
506834c881
Extend logging middleware with more config options 2025-05-04 19:28:03 +02:00
4b8a62ba12
Add some more documentation and links to godoc# 2025-04-28 08:49:13 +02:00
fa857615dd
Maybe fix cov img 2025-04-24 16:24:16 +02:00
8156b5b8eb
Update readme 2025-04-24 16:23:30 +02:00
92e881b3e4
Add coverage badge 2025-04-24 16:01:37 +02:00
ccf98c2f6e
Shit ton of tests 2025-04-24 15:59:15 +02:00
99b00887a8
Apply some guy's law about logic operation reordering 2025-04-24 15:58:32 +02:00
695b48ae91
More error returns added 2025-04-24 15:58:21 +02:00
2b737d056c
Add err returns 2025-04-24 15:58:06 +02:00
4013622afc
Remove logrotate, lumberjack is better 2025-04-24 15:57:49 +02:00
8bb1797894
Remove multiplexer, since wrong implemented 2025-04-24 15:57:30 +02:00
be9ed2adef
Remove mutex, since no usages 2025-04-24 15:57:14 +02:00
09de0a19e1
Change queues and stacks
- Make chain element private
- Remove JSON Marshaller from stack and queue
- Remove access to top chain element
2025-04-24 15:52:43 +02:00
9870d87d41
Add sliceutils.ContainsFunc 2025-04-11 12:38:41 +02:00
15887d9d2e
Add builder for Logging middleware
Builder offers option to add extra fields to log messages
2025-04-11 10:44:50 +02:00
e8aa16622b
Add generic math abs 2025-04-10 14:36:51 +02:00
4612ee993e
BREAKING CHANGE: Rename http module to webutils 2025-04-09 13:57:01 +02:00
32 changed files with 1427 additions and 314 deletions

View file

@ -1,3 +1,8 @@
# GoUtils
![Go Coverage](https://git.mstar.dev/mstar/goutils/raw/branch/main/coverage_badge.png)
[![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://git.mstar.dev/mstar/goutils/src/branch/main/LICENSE)
[![Go Documentation](https://godocs.io/git.mstar.dev/mstar/goutils?status.svg)](https://godocs.io/git.mstar.dev/mstar/goutils)
[![GoDoc](https://pkg.go.dev/badge/git.mstar.dev/mstar/goutils)](https://pkg.go.dev/git.mstar.dev/mstar/goutils)
A collection of useful functions and structs that I use for my projects.

View file

@ -1,15 +1,16 @@
// Package containers contains implementations of a generic queue and stack
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 +27,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,
}

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1,3 +1,5 @@
// Package embedFsWrapper contains a wrapper around [io/fs.FS] for working
// around a limitation of the [embed] package
package embedFsWrapper
import (

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

View file

@ -1,4 +1,6 @@
package http
// Package webutils contains various functions for easier interaction and common tasks
// when working with [net/http.Handler] based webservers
package webutils
import (
"net/http"

33
http/chain_test.go Normal file
View 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)
}

View file

@ -1,4 +1,4 @@
package http
package webutils
import (
"context"

23
http/context_test.go Normal file
View 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
}

View file

@ -1,4 +1,4 @@
package http
package webutils
import (
"encoding/json"
@ -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
View 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)
}
}

20
http/json.go Normal file
View file

@ -0,0 +1,20 @@
package webutils
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 other.Error("webutils.SendJson", "failed to marshal data to json", err)
}
w.Header().Add("Content-Type", "application/json")
// Can't really catch here
_, _ = fmt.Fprint(w, string(encoded))
return nil
}

45
http/json_test.go Normal file
View 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")
}
}

78
http/sse.go Normal file
View file

@ -0,0 +1,78 @@
package webutils
import (
"errors"
"fmt"
"net/http"
"strings"
)
type SseEvent struct {
Type *string
Id *string
Data string
}
var (
ErrWriterNotFlushable = errors.New("response writer not flushable")
ErrEventTypeNoNewline = errors.New("event type contained a newline")
ErrEventIdNoNewline = errors.New("event id contained a newline")
)
// SseWriter provides a simple implementation for sending data via Server Side Events
// to the client. The function returns once the dataStream channel is closed.
// The ResponseWriter should not be used after calling this function
//
// Event rules:
// - SseEvent.Type must not contain any newlines
// - SseEvent.Id must not contain any newlines
// - "\r\n" in SseEvent.Data will be replaced with "\n", afterwards "\r" will also be replaced with "\n"
//
// Inspired by and partially copied from https://medium.com/@rian.eka.cahya/server-sent-event-sse-with-go-10592d9c2aa1
// Also why this and not github.com/tmaxmax/go-sse?
// go-sse is not flexible in its use case. You have one handler that doesn't differentiate between clients.
// This, of course, does not work when you have to send individualised notifications as everyone would
// get the same messages, causing confusion in the best case and data leaks in the worst case
func SseWriter(w http.ResponseWriter, dataStream chan SseEvent) error {
w.Header().Set("Access-Control-Expose-Headers", "Content-Type")
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
return ErrWriterNotFlushable
}
for data := range dataStream {
if err := ValidateSseEvent(&data); err != nil {
return err
}
if data.Type != nil {
_, _ = fmt.Fprintf(w, "event: %s\n", *data.Type)
}
if data.Id != nil {
_, _ = fmt.Fprintf(w, "id: %s\n", *data.Id)
}
noCr := strings.ReplaceAll(data.Data, "\r\n", "\n")
noCr = strings.ReplaceAll(noCr, "\r", "\n")
for _, line := range strings.Split(noCr, "\n") {
_, _ = fmt.Fprintf(w, "data: %s\n", line)
}
_, _ = fmt.Fprint(w, "\n\n")
flusher.Flush()
}
return nil
}
// Check if a given event is valid for sending via SseWriter
// This function is also internally called by SseWriter for each outbound event
func ValidateSseEvent(e *SseEvent) error {
if e.Type != nil && strings.ContainsRune(*e.Type, '\n') {
return ErrEventTypeNoNewline
}
if e.Id != nil && strings.ContainsRune(*e.Id, '\n') {
return ErrEventIdNoNewline
}
return nil
}

View file

@ -1,14 +1,58 @@
package http
package webutils
import (
"net/http"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
)
func BuildLoggingMiddleware(
status500IsError bool,
ignorePaths []string,
extras map[string]string,
) HandlerBuilder {
return func(h http.Handler) http.Handler {
return ChainMiddlewares(h,
hlog.NewHandler(log.Logger),
hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
for _, p := range ignorePaths {
if strings.HasPrefix(r.URL.Path, p) {
return
}
}
var logger *zerolog.Event
if status >= 500 {
if status500IsError {
logger = hlog.FromRequest(r).Error()
} else {
logger = hlog.FromRequest(r).Warn()
}
} else {
logger = hlog.FromRequest(r).Info()
}
logger = logger.
Str("method", r.Method).
Stringer("url", r.URL).
Int("status", status).
Int("size", size).
Dur("duration", duration)
for k, v := range extras {
logger = logger.Str(k, v)
}
logger.Send()
}),
RealIpAppenderMiddleware("ip"),
hlog.UserAgentHandler("user_agent"),
hlog.RefererHandler("referer"),
hlog.RequestIDHandler("req_id", "Request-Id"),
)
}
}
func LoggingMiddleware(handler http.Handler) http.Handler {
return ChainMiddlewares(handler,
hlog.NewHandler(log.Logger),
@ -24,9 +68,33 @@ func LoggingMiddleware(handler http.Handler) http.Handler {
Dur("duration", duration).
Send()
}),
hlog.RemoteAddrHandler("ip"),
RealIpAppenderMiddleware("ip"),
hlog.UserAgentHandler("user_agent"),
hlog.RefererHandler("referer"),
hlog.RequestIDHandler("req_id", "Request-Id"),
)
}
// hlog.RemoteAddrHandler except fixed to check the X-Real-Ip and X-Forwarded-For
// headers first for the IP instead of relying on RemoteAddr
// (which would only return the last proxy's address instead of the caller's)
func RealIpAppenderMiddleware(fieldKey string) func(handler http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
}
if IPAddress != "" {
log := zerolog.Ctx(r.Context())
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str(fieldKey, IPAddress)
})
}
next.ServeHTTP(w, r)
})
}
}

View file

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

View file

@ -1,3 +1,4 @@
// Package maputils contains various generic functions for applying an operation on an entire map
package maputils
import "git.mstar.dev/mstar/goutils/sliceutils"
@ -65,13 +66,15 @@ 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
}
}
return true
}
// Compact reduces the keys and values of a map down into one value each.
// The starting value for each is the default value of that type.
func Compact[K comparable, V any](
m map[K]V,
compactor func(accK K, accV V, nextK K, nextV V) (K, V),

274
maputils/mapUtils_test.go Normal file
View 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)
}
})
}
}

14
math/math.go Normal file
View file

@ -0,0 +1,14 @@
// Package mathutils contains helper functions for performing common mathematical operations
package mathutils
type SignedNumber interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}
func Abs[T SignedNumber](num T) T {
if num > 0 {
return num
} else {
return num * -1
}
}

16
math/math_test.go Normal file
View 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)
}
}

View file

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

View file

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

View file

@ -1,3 +1,4 @@
// Package other contains various funtions that didn't fit into any of the other packages
package other
import "fmt"

55
other/other_test.go Normal file
View 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)
}
}

View file

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

View file

@ -1,6 +1,7 @@
// Package sliceutils contains various generic functions for applying an operation across an entire slice
package sliceutils
// MapS applies a given function to every element of a slice.
// Map applies a given function to every element of a slice.
// The return type may be different from the initial type of the slice.
func Map[T any, M any](arr []T, apply func(T) M) []M {
n := make([]M, len(arr))
@ -72,6 +73,7 @@ func CompareUnordered[T comparable](a, b []T) bool {
return hits == len(a)
}
// Returns whether b exists inside a using a simple == comparison
func Contains[T comparable](a []T, b T) bool {
for _, v := range a {
if v == b {
@ -81,6 +83,18 @@ func Contains[T comparable](a []T, b T) bool {
return false
}
// Returns whether the given function f returns true for any element in a
func ContainsFunc[T any](a []T, f func(t T) bool) bool {
for _, v := range a {
if f(v) {
return true
}
}
return false
}
// Compact the slice a using the compactor function.
// For the first call, the accumulator argument will be the default value
func Compact[T any](a []T, compactor func(acc T, next T) T) T {
var acc T
for _, v := range a {
@ -88,3 +102,28 @@ func Compact[T any](a []T, compactor func(acc T, next T) T) T {
}
return acc
}
// Returns a channel that all elements in a will be written to in order.
// Once all values of a have been sent, the channel will be closed.
// The channel must be fully consumed until closed. Otherwise a goroutine will be leaked
func ToChannel[T any](a []T) <-chan T {
c := make(chan T)
go func() {
for _, v := range a {
c <- v
}
close(c)
}()
return c
}
// FromChannel reads from a channel until closed, appending every element to a slice.
// If you do not know how many elements to expect, use an expectedSize of 0
func FromChannel[T any](c <-chan T, expectedSize uint) []T {
a := make([]T, expectedSize)
for v := range c {
a = append(a, v)
}
return a
}

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

View file

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