v0.5.0-rc5: check release

This commit is contained in:
yusing
2024-09-19 20:40:03 +08:00
parent be7a766cb2
commit 4a2d42bfa9
68 changed files with 1971 additions and 1107 deletions

View File

@@ -1,229 +1,116 @@
package functional
import (
"context"
"sync"
"github.com/puzpuzpuz/xsync/v3"
"gopkg.in/yaml.v3"
E "github.com/yusing/go-proxy/error"
)
type Map[KT comparable, VT any] struct {
m map[KT]VT
defVals map[KT]VT
sync.RWMutex
*xsync.MapOf[KT, VT]
}
// NewMap creates a new Map with the given map as its initial values.
//
// Parameters:
// - dv: optional default values for the Map
//
// Return:
// - *Map[KT, VT]: a pointer to the newly created Map.
func NewMap[KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] {
return NewMapFrom(make(map[KT]VT), dv...)
func NewMapOf[KT comparable, VT any](options ...func(*xsync.MapConfig)) Map[KT, VT] {
return Map[KT, VT]{xsync.NewMapOf[KT, VT](options...)}
}
// NewMapOf creates a new Map with the given map as its initial values.
//
// Type parameters:
// - M: type for the new map.
//
// Parameters:
// - dv: optional default values for the Map
//
// Return:
// - *Map[KT, VT]: a pointer to the newly created Map.
func NewMapOf[M Map[KT, VT], KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] {
return NewMapFrom(make(map[KT]VT), dv...)
}
// NewMapFrom creates a new Map with the given map as its initial values.
//
// Parameters:
// - from: a map of type KT to VT, which will be the initial values of the Map.
// - dv: optional default values for the Map
//
// Return:
// - *Map[KT, VT]: a pointer to the newly created Map.
func NewMapFrom[KT comparable, VT any](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] {
if len(dv) > 0 {
return &Map[KT, VT]{m: from, defVals: dv[0]}
func NewMapFrom[KT comparable, VT any](m map[KT]VT) (res Map[KT, VT]) {
res = NewMapOf[KT, VT](xsync.WithPresize(len(m)))
for k, v := range m {
res.Store(k, v)
}
return &Map[KT, VT]{m: from}
return
}
func (m *Map[KT, VT]) Set(key KT, value VT) {
m.Lock()
m.m[key] = value
m.Unlock()
}
func MapFind[KT comparable, VT, CT any](m Map[KT, VT], criteria func(VT) (CT, bool)) (_ CT) {
result := make(chan CT, 1)
func (m *Map[KT, VT]) Get(key KT) VT {
m.RLock()
defer m.RUnlock()
value, ok := m.m[key]
if !ok && m.defVals != nil {
return m.defVals[key]
}
return value
}
// Find searches for the first element in the map that satisfies the given criteria.
//
// Parameters:
// - criteria: a function that takes a value of type VT and returns a tuple of any type and a boolean.
//
// Return:
// - any: the first value that satisfies the criteria, or nil if no match is found.
func (m *Map[KT, VT]) Find(criteria func(VT) (any, bool)) any {
m.RLock()
defer m.RUnlock()
result := make(chan any)
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for _, v := range m.m {
wg.Add(1)
go func(val VT) {
defer wg.Done()
if value, ok := criteria(val); ok {
select {
case result <- value:
cancel() // Cancel other goroutines if a result is found
case <-ctx.Done(): // If already cancelled
return
}
m.Range(func(key KT, value VT) bool {
select {
case <-result: // already have a result
return false // stop iteration
default:
if got, ok := criteria(value); ok {
result <- got
return false
}
}(v)
}
go func() {
wg.Wait()
close(result)
}()
// The first valid match, if any
select {
case res, ok := <-result:
if ok {
return res
return true
}
case <-ctx.Done():
})
select {
case v := <-result:
return v
default:
return
}
return nil // Return nil if no matches found
}
func (m *Map[KT, VT]) UnsafeGet(key KT) (VT, bool) {
value, ok := m.m[key]
return value, ok
}
func (m *Map[KT, VT]) UnsafeSet(key KT, value VT) {
m.m[key] = value
}
func (m *Map[KT, VT]) Delete(key KT) {
m.Lock()
delete(m.m, key)
m.Unlock()
}
func (m *Map[KT, VT]) UnsafeDelete(key KT) {
delete(m.m, key)
}
// MergeWith merges the contents of another Map[KT, VT]
// into the current Map[KT, VT] and
// returns a map that were duplicated.
// MergeFrom add contents from another `Map`, ignore duplicated keys
//
// Parameters:
// - other: a pointer to another Map[KT, VT] to be merged into the current Map[KT, VT].
// - other: `Map` of values to add from
//
// Return:
// - Map[KT, VT]: a map of key-value pairs that were duplicated during the merge.
func (m *Map[KT, VT]) MergeWith(other *Map[KT, VT]) Map[KT, VT] {
dups := make(map[KT]VT)
// - Map: a `Map` of duplicated keys-value pairs
func (m Map[KT, VT]) MergeFrom(other Map[KT, VT]) Map[KT, VT] {
dups := NewMapOf[KT, VT]()
m.Lock()
for k, v := range other.m {
if _, isDup := m.m[k]; !isDup {
m.m[k] = v
other.Range(func(k KT, v VT) bool {
if _, ok := m.Load(k); ok {
dups.Store(k, v)
} else {
dups[k] = v
m.Store(k, v)
}
}
m.Unlock()
return Map[KT, VT]{m: dups}
return true
})
return dups
}
func (m *Map[KT, VT]) Clear() {
m.Lock()
m.m = make(map[KT]VT)
m.Unlock()
func (m Map[KT, VT]) RangeAll(do func(k KT, v VT)) {
m.Range(func(k KT, v VT) bool {
do(k, v)
return true
})
}
func (m *Map[KT, VT]) Size() int {
m.RLock()
defer m.RUnlock()
return len(m.m)
func (m Map[KT, VT]) RemoveAll(criteria func(VT) bool) {
m.Range(func(k KT, v VT) bool {
if criteria(v) {
m.Delete(k)
}
return true
})
}
func (m *Map[KT, VT]) Contains(key KT) bool {
m.RLock()
_, ok := m.m[key]
m.RUnlock()
func (m Map[KT, VT]) Has(k KT) bool {
_, ok := m.Load(k)
return ok
}
func (m *Map[KT, VT]) Clone() *Map[KT, VT] {
m.RLock()
defer m.RUnlock()
clone := make(map[KT]VT, len(m.m))
for k, v := range m.m {
clone[k] = v
func (m Map[KT, VT]) UnmarshalFromYAML(data []byte) E.NestedError {
if m.Size() != 0 {
return E.FailedWhy("unmarshal from yaml", "map is not empty")
}
return &Map[KT, VT]{m: clone, defVals: m.defVals}
}
func (m *Map[KT, VT]) EachKV(fn func(k KT, v VT)) {
m.Lock()
for k, v := range m.m {
fn(k, v)
tmp := make(map[KT]VT)
if err := E.From(yaml.Unmarshal(data, tmp)); err.HasError() {
return err
}
m.Unlock()
}
func (m *Map[KT, VT]) Each(fn func(v VT)) {
m.Lock()
for _, v := range m.m {
fn(v)
for k, v := range tmp {
m.Store(k, v)
}
m.Unlock()
return nil
}
func (m *Map[KT, VT]) EachParallel(fn func(v VT)) {
m.Lock()
ParallelForEachValue(m.m, fn)
m.Unlock()
}
func (m *Map[KT, VT]) EachKVParallel(fn func(k KT, v VT)) {
m.Lock()
ParallelForEachKV(m.m, fn)
m.Unlock()
}
func (m *Map[KT, VT]) UnmarshalFromYAML(data []byte) E.NestedError {
return E.From(yaml.Unmarshal(data, m.m))
}
func (m *Map[KT, VT]) Iterator() map[KT]VT {
return m.m
func (m Map[KT, VT]) String() string {
tmp := make(map[KT]VT, m.Size())
m.RangeAll(func(k KT, v VT) {
tmp[k] = v
})
data, err := yaml.Marshal(tmp)
if err != nil {
return err.Error()
}
return string(data)
}

View File

@@ -0,0 +1,75 @@
package functional_test
import (
"testing"
. "github.com/yusing/go-proxy/utils/functional"
. "github.com/yusing/go-proxy/utils/testing"
)
func TestNewMapFrom(t *testing.T) {
m := NewMapFrom(map[string]int{
"a": 1,
"b": 2,
"c": 3,
})
ExpectEqual(t, m.Size(), 3)
ExpectTrue(t, m.Has("a"))
ExpectTrue(t, m.Has("b"))
ExpectTrue(t, m.Has("c"))
}
func TestMapFind(t *testing.T) {
m := NewMapFrom(map[string]map[string]int{
"a": {
"a": 1,
},
"b": {
"a": 1,
"b": 2,
},
"c": {
"b": 2,
"c": 3,
},
})
res := MapFind(m, func(inner map[string]int) (int, bool) {
if _, ok := inner["c"]; ok && inner["c"] == 3 {
return inner["c"], true
}
return 0, false
})
ExpectEqual(t, res, 3)
}
func TestMergeFrom(t *testing.T) {
m1 := NewMapFrom(map[string]int{
"a": 1,
"b": 2,
"c": 3,
"d": 4,
})
m2 := NewMapFrom(map[string]int{
"a": 1,
"c": 123,
"e": 456,
"f": 6,
})
dup := m1.MergeFrom(m2)
ExpectEqual(t, m1.Size(), 6)
ExpectTrue(t, m1.Has("e"))
ExpectTrue(t, m1.Has("f"))
c, _ := m1.Load("c")
d, _ := m1.Load("d")
e, _ := m1.Load("e")
f, _ := m1.Load("f")
ExpectEqual(t, c, 3)
ExpectEqual(t, d, 4)
ExpectEqual(t, e, 456)
ExpectEqual(t, f, 6)
ExpectEqual(t, dup.Size(), 2)
ExpectTrue(t, dup.Has("a"))
ExpectTrue(t, dup.Has("c"))
}

View File

@@ -10,15 +10,8 @@ import (
E "github.com/yusing/go-proxy/error"
)
// TODO: move to "utils/io"
type (
Reader interface {
Read() ([]byte, E.NestedError)
}
StdReader struct {
r Reader
}
FileReader struct {
Path string
}
@@ -29,13 +22,6 @@ type (
closed atomic.Bool
}
StdReadCloser struct {
r *ReadCloser
}
ByteReader []byte
NewByteReader = ByteReader
Pipe struct {
r ReadCloser
w io.WriteCloser
@@ -44,49 +30,25 @@ type (
}
BidirectionalPipe struct {
pSrcDst Pipe
pDstSrc Pipe
pSrcDst *Pipe
pDstSrc *Pipe
}
)
func NewFileReader(path string) *FileReader {
return &FileReader{Path: path}
}
func (r StdReader) Read() ([]byte, error) {
return r.r.Read()
}
func (r *FileReader) Read() ([]byte, E.NestedError) {
return E.Check(os.ReadFile(r.Path))
}
func (r ByteReader) Read() ([]byte, E.NestedError) {
return r, E.Nil()
}
func (r *ReadCloser) Read(p []byte) (int, E.NestedError) {
func (r *ReadCloser) Read(p []byte) (int, error) {
select {
case <-r.ctx.Done():
return 0, E.From(r.ctx.Err())
return 0, r.ctx.Err()
default:
return E.Check(r.r.Read(p))
return r.r.Read(p)
}
}
func (r *ReadCloser) Close() E.NestedError {
func (r *ReadCloser) Close() error {
if r.closed.Load() {
return E.Nil()
return nil
}
r.closed.Store(true)
return E.From(r.r.Close())
}
func (r StdReadCloser) Read(p []byte) (int, error) {
return r.r.Read(p)
}
func (r StdReadCloser) Close() error {
return r.r.Close()
}
@@ -100,35 +62,35 @@ func NewPipe(ctx context.Context, r io.ReadCloser, w io.WriteCloser) *Pipe {
}
}
func (p *Pipe) Start() E.NestedError {
return Copy(p.ctx, p.w, &StdReadCloser{&p.r})
func (p *Pipe) Start() error {
return Copy(p.ctx, p.w, &p.r)
}
func (p *Pipe) Stop() E.NestedError {
func (p *Pipe) Stop() error {
p.cancel()
return E.Join("error stopping pipe", p.r.Close(), p.w.Close())
return E.JoinE("error stopping pipe", p.r.Close(), p.w.Close()).Error()
}
func (p *Pipe) Write(b []byte) (int, E.NestedError) {
return E.Check(p.w.Write(b))
func (p *Pipe) Write(b []byte) (int, error) {
return p.w.Write(b)
}
func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) *BidirectionalPipe {
return &BidirectionalPipe{
pSrcDst: *NewPipe(ctx, rw1, rw2),
pDstSrc: *NewPipe(ctx, rw2, rw1),
pSrcDst: NewPipe(ctx, rw1, rw2),
pDstSrc: NewPipe(ctx, rw2, rw1),
}
}
func NewBidirectionalPipeIntermediate(ctx context.Context, listener io.ReadCloser, client io.ReadWriteCloser, target io.ReadWriteCloser) *BidirectionalPipe {
return &BidirectionalPipe{
pSrcDst: *NewPipe(ctx, listener, client),
pDstSrc: *NewPipe(ctx, client, target),
pSrcDst: NewPipe(ctx, listener, client),
pDstSrc: NewPipe(ctx, client, target),
}
}
func (p *BidirectionalPipe) Start() E.NestedError {
errCh := make(chan E.NestedError, 2)
func (p *BidirectionalPipe) Start() error {
errCh := make(chan error, 2)
go func() {
errCh <- p.pSrcDst.Start()
}()
@@ -136,34 +98,34 @@ func (p *BidirectionalPipe) Start() E.NestedError {
errCh <- p.pDstSrc.Start()
}()
for err := range errCh {
if err.HasError() {
if err != nil {
return err
}
}
return E.Nil()
return nil
}
func (p *BidirectionalPipe) Stop() E.NestedError {
return E.Join("error stopping pipe", p.pSrcDst.Stop(), p.pDstSrc.Stop())
func (p *BidirectionalPipe) Stop() error {
return E.JoinE("error stopping pipe", p.pSrcDst.Stop(), p.pDstSrc.Stop()).Error()
}
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) E.NestedError {
_, err := io.Copy(dst, StdReadCloser{&ReadCloser{ctx: ctx, r: src}})
return E.From(err)
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) error {
_, err := io.Copy(dst, &ReadCloser{ctx: ctx, r: src})
return err
}
func LoadJson[T any](path string, pointer *T) E.NestedError {
data, err := os.ReadFile(path)
if err != nil {
return E.From(err)
data, err := E.Check(os.ReadFile(path))
if err.HasError() {
return err
}
return E.From(json.Unmarshal(data, pointer))
}
func SaveJson[T any](path string, pointer *T, perm os.FileMode) E.NestedError {
data, err := json.Marshal(pointer)
if err != nil {
return E.From(err)
data, err := E.Check(json.Marshal(pointer))
if err.HasError() {
return err
}
return E.From(os.WriteFile(path, data, perm))
}

View File

@@ -20,5 +20,5 @@ func SetFieldFromSnake[T, VT any](obj *T, field string, value VT) E.NestedError
return E.Invalid("field", field)
}
prop.Set(reflect.ValueOf(value))
return E.Nil()
return nil
}

View File

@@ -17,45 +17,26 @@ func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
err := yaml.Unmarshal(data, &i)
if err != nil {
return E.Failure("unmarshal yaml").With(err)
return E.FailWith("unmarshal yaml", err)
}
m, err := json.Marshal(i)
if err != nil {
return E.Failure("marshal json").With(err)
return E.FailWith("marshal json", err)
}
err = schema.Validate(bytes.NewReader(m))
if err == nil {
return E.Nil()
return nil
}
errors := E.NewBuilder("yaml validation error")
for _, e := range err.(*jsonschema.ValidationError).Causes {
errors.Add(e)
errors.AddE(e)
}
return errors.Build()
}
// TryJsonStringify converts the given object to a JSON string.
//
// It takes an object of any type and attempts to marshal it into a JSON string.
// If the marshaling is successful, the JSON string is returned.
// If the marshaling fails, the object is converted to a string using fmt.Sprint and returned.
//
// Parameters:
// - o: The object to be converted to a JSON string.
//
// Return type:
// - string: The JSON string representation of the object.
func TryJsonStringify(o any) string {
b, err := json.Marshal(o)
if err != nil {
return fmt.Sprint(o)
}
return string(b)
}
// Serialize converts the given data into a map[string]any representation.
//
// It uses reflection to inspect the data type and handle different kinds of data.
@@ -123,7 +104,7 @@ func Serialize(data any) (SerializedObject, E.NestedError) {
return nil, E.Unsupported("type", value.Kind())
}
return result, E.Nil()
return result, nil
}
func Deserialize(src map[string]any, target any) E.NestedError {
@@ -166,7 +147,7 @@ func Deserialize(src map[string]any, target any) E.NestedError {
propNew := reflect.New(propType.Elem())
err := Deserialize(vSerialized, propNew.Interface())
if err.HasError() {
return E.Failure("set field").With(k).With(err)
return E.Failure("set field").With(err).Subject(k)
}
prop.Set(propNew)
default:
@@ -180,7 +161,15 @@ func Deserialize(src map[string]any, target any) E.NestedError {
}
}
return E.Nil()
return nil
}
func DeserializeJson(j map[string]string, target any) E.NestedError {
data, err := E.Check(json.Marshal(j))
if err.HasError() {
return err
}
return E.From(json.Unmarshal(data, target))
}
func toLowerNoSnake(s string) string {

11
src/utils/string.go Normal file
View File

@@ -0,0 +1,11 @@
package utils
import "strings"
func CommaSeperatedList(s string) []string {
res := strings.Split(s, ",")
for i, part := range res {
res[i] = strings.TrimSpace(part)
}
return res
}

View File

@@ -3,25 +3,23 @@ package utils
import (
"reflect"
"testing"
E "github.com/yusing/go-proxy/error"
)
func ExpectNoError(t *testing.T, err error) {
t.Helper()
var noError bool
switch t := err.(type) {
case E.NestedError:
noError = t.NoError()
default:
noError = err == nil
}
if !noError {
if err != nil && !reflect.ValueOf(err).IsNil() {
t.Errorf("expected err=nil, got %s", err.Error())
}
}
func ExpectEqual(t *testing.T, got, want any) {
func ExpectEqual[T comparable](t *testing.T, got T, want T) {
t.Helper()
if got != want {
t.Errorf("expected:\n%v, got\n%v", want, got)
}
}
func ExpectDeepEqual[T any](t *testing.T, got T, want T) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("expected:\n%v, got\n%v", want, got)
@@ -47,7 +45,7 @@ func ExpectType[T any](t *testing.T, got any) T {
tExpect := reflect.TypeFor[T]()
_, ok := got.(T)
if !ok {
t.Errorf("expected type %T, got %T", tExpect, got)
t.Errorf("expected type %s, got %T", tExpect, got)
}
return got.(T)
}