restructured the project to comply community guideline, for others check release note

This commit is contained in:
yusing
2024-09-28 09:51:34 +08:00
parent 4120fd8d1c
commit 90487bfde6
134 changed files with 1110 additions and 625 deletions

59
internal/utils/format.go Normal file
View File

@@ -0,0 +1,59 @@
package utils
import (
"fmt"
"strings"
"time"
)
func FormatDuration(d time.Duration) string {
// Get total seconds from duration
totalSeconds := int64(d.Seconds())
// Calculate days, hours, minutes, and seconds
days := totalSeconds / (24 * 3600)
hours := (totalSeconds % (24 * 3600)) / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60
// Create a slice to hold parts of the duration
var parts []string
if days > 0 {
parts = append(parts, fmt.Sprintf("%d day%s", days, pluralize(days)))
}
if hours > 0 {
parts = append(parts, fmt.Sprintf("%d hour%s", hours, pluralize(hours)))
}
if minutes > 0 {
parts = append(parts, fmt.Sprintf("%d minute%s", minutes, pluralize(minutes)))
}
if seconds > 0 {
parts = append(parts, fmt.Sprintf("%d second%s", seconds, pluralize(seconds)))
}
// Join the parts with appropriate connectors
if len(parts) == 0 {
return "0 Seconds"
}
if len(parts) == 1 {
return parts[0]
}
return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1]
}
func ParseBool(s string) bool {
switch strings.ToLower(s) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
func pluralize(n int64) string {
if n > 1 {
return "s"
}
return ""
}

34
internal/utils/fs.go Normal file
View File

@@ -0,0 +1,34 @@
package utils
import (
"fmt"
"os"
"path"
)
// Recursively lists all files in a directory until `maxDepth` is reached
// Returns a slice of file paths relative to `dir`
func ListFiles(dir string, maxDepth int) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("error listing directory %s: %w", dir, err)
}
files := make([]string, 0)
for _, entry := range entries {
if entry.IsDir() {
if maxDepth <= 0 {
continue
}
subEntries, err := ListFiles(path.Join(dir, entry.Name()), maxDepth-1)
if err != nil {
return nil, err
}
for _, subEntry := range subEntries {
files = append(files, path.Join(dir, entry.Name(), subEntry))
}
} else {
files = append(files, path.Join(dir, entry.Name()))
}
}
return files, nil
}

View File

@@ -0,0 +1,69 @@
package functional
import "sync"
func ForEachKey[K comparable, V any](obj map[K]V, do func(K)) {
for k := range obj {
do(k)
}
}
func ForEachValue[K comparable, V any](obj map[K]V, do func(V)) {
for _, v := range obj {
do(v)
}
}
func ForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) {
for k, v := range obj {
do(k, v)
}
}
func ParallelForEach[T any](obj []T, do func(T)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for _, v := range obj {
go func(v T) {
do(v)
wg.Done()
}(v)
}
wg.Wait()
}
func ParallelForEachKey[K comparable, V any](obj map[K]V, do func(K)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for k := range obj {
go func(k K) {
do(k)
wg.Done()
}(k)
}
wg.Wait()
}
func ParallelForEachValue[K comparable, V any](obj map[K]V, do func(V)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for _, v := range obj {
go func(v V) {
do(v)
wg.Done()
}(v)
}
wg.Wait()
}
func ParallelForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) {
var wg sync.WaitGroup
wg.Add(len(obj))
for k, v := range obj {
go func(k K, v V) {
do(k, v)
wg.Done()
}(k, v)
}
wg.Wait()
}

View File

@@ -0,0 +1,116 @@
package functional
import (
"github.com/puzpuzpuz/xsync/v3"
"gopkg.in/yaml.v3"
E "github.com/yusing/go-proxy/internal/error"
)
type Map[KT comparable, VT any] struct {
*xsync.MapOf[KT, VT]
}
func NewMapOf[KT comparable, VT any](options ...func(*xsync.MapConfig)) Map[KT, VT] {
return Map[KT, VT]{xsync.NewMapOf[KT, VT](options...)}
}
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
}
func MapFind[KT comparable, VT, CT any](m Map[KT, VT], criteria func(VT) (CT, bool)) (_ CT) {
result := make(chan CT, 1)
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
}
return true
}
})
select {
case v := <-result:
return v
default:
return
}
}
// MergeFrom add contents from another `Map`, ignore duplicated keys
//
// Parameters:
// - other: `Map` of values to add from
//
// Return:
// - 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]()
other.Range(func(k KT, v VT) bool {
if _, ok := m.Load(k); ok {
dups.Store(k, v)
} else {
m.Store(k, v)
}
return true
})
return dups
}
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]) 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]) Has(k KT) bool {
_, ok := m.Load(k)
return ok
}
func (m Map[KT, VT]) UnmarshalFromYAML(data []byte) E.NestedError {
if m.Size() != 0 {
return E.FailedWhy("unmarshal from yaml", "map is not empty")
}
tmp := make(map[KT]VT)
if err := E.From(yaml.Unmarshal(data, tmp)); err.HasError() {
return err
}
for k, v := range tmp {
m.Store(k, v)
}
return nil
}
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/internal/utils/functional"
. "github.com/yusing/go-proxy/internal/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

@@ -0,0 +1,8 @@
package functional
func FirstValueOf[KT comparable, VT any](m map[KT]VT) (_ VT, ok bool) {
for _, v := range m {
return v, true
}
return
}

View File

@@ -0,0 +1,71 @@
package functional
type Slice[T any] struct {
s []T
}
func NewSlice[T any]() *Slice[T] {
return &Slice[T]{make([]T, 0)}
}
func NewSliceN[T any](n int) *Slice[T] {
return &Slice[T]{make([]T, n)}
}
func NewSliceFrom[T any](s []T) *Slice[T] {
return &Slice[T]{s}
}
func (s *Slice[T]) Size() int {
return len(s.s)
}
func (s *Slice[T]) Empty() bool {
return len(s.s) == 0
}
func (s *Slice[T]) NotEmpty() bool {
return len(s.s) > 0
}
func (s *Slice[T]) Iterator() []T {
return s.s
}
func (s *Slice[T]) Set(i int, v T) {
s.s[i] = v
}
func (s *Slice[T]) Add(e T) *Slice[T] {
s.s = append(s.s, e)
return s
}
func (s *Slice[T]) AddRange(other *Slice[T]) *Slice[T] {
s.s = append(s.s, other.s...)
return s
}
func (s *Slice[T]) ForEach(do func(T)) {
for _, v := range s.s {
do(v)
}
}
func (s *Slice[T]) Map(m func(T) T) *Slice[T] {
n := make([]T, len(s.s))
for i, v := range s.s {
n[i] = m(v)
}
return &Slice[T]{n}
}
func (s *Slice[T]) Filter(f func(T) bool) *Slice[T] {
n := make([]T, 0)
for _, v := range s.s {
if f(v) {
n = append(n, v)
}
}
return &Slice[T]{n}
}

127
internal/utils/io.go Normal file
View File

@@ -0,0 +1,127 @@
package utils
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"syscall"
E "github.com/yusing/go-proxy/internal/error"
)
// TODO: move to "utils/io"
type (
FileReader struct {
Path string
}
ContextReader struct {
ctx context.Context
io.Reader
}
ContextWriter struct {
ctx context.Context
io.Writer
}
Pipe struct {
r ContextReader
w ContextWriter
ctx context.Context
cancel context.CancelFunc
}
BidirectionalPipe struct {
pSrcDst *Pipe
pDstSrc *Pipe
}
)
func (r *ContextReader) Read(p []byte) (int, error) {
select {
case <-r.ctx.Done():
return 0, r.ctx.Err()
default:
return r.Reader.Read(p)
}
}
func (w *ContextWriter) Write(p []byte) (int, error) {
select {
case <-w.ctx.Done():
return 0, w.ctx.Err()
default:
return w.Writer.Write(p)
}
}
func NewPipe(ctx context.Context, r io.ReadCloser, w io.WriteCloser) *Pipe {
_, cancel := context.WithCancel(ctx)
return &Pipe{
r: ContextReader{ctx: ctx, Reader: r},
w: ContextWriter{ctx: ctx, Writer: w},
ctx: ctx,
cancel: cancel,
}
}
func (p *Pipe) Start() (err error) {
err = Copy(&p.w, &p.r)
switch {
case
// NOTE: ignoring broken pipe and connection reset by peer
errors.Is(err, syscall.EPIPE),
errors.Is(err, syscall.ECONNRESET):
return nil
}
return err
}
func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) BidirectionalPipe {
return BidirectionalPipe{
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),
}
}
func (p BidirectionalPipe) Start() error {
errCh := make(chan error, 2)
go func() {
errCh <- p.pSrcDst.Start()
}()
go func() {
errCh <- p.pDstSrc.Start()
}()
return E.JoinE("bidirectional pipe error", <-errCh, <-errCh).Error()
}
func Copy(dst *ContextWriter, src *ContextReader) error {
_, err := io.Copy(dst, src)
return err
}
func LoadJson[T any](path string, pointer *T) E.NestedError {
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 := E.Check(json.Marshal(pointer))
if err.HasError() {
return err
}
return E.From(os.WriteFile(path, data, perm))
}

8
internal/utils/must.go Normal file
View File

@@ -0,0 +1,8 @@
package utils
func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}

19
internal/utils/schema.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
import (
"github.com/santhosh-tekuri/jsonschema"
)
var (
schemaCompiler = jsonschema.NewCompiler()
schemaStorage = make(map[string]*jsonschema.Schema)
)
func GetSchema(path string) *jsonschema.Schema {
if schema, ok := schemaStorage[path]; ok {
return schema
}
schema := schemaCompiler.MustCompile(path)
schemaStorage[path] = schema
return schema
}

View File

@@ -0,0 +1,182 @@
package utils
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/santhosh-tekuri/jsonschema"
E "github.com/yusing/go-proxy/internal/error"
"gopkg.in/yaml.v3"
)
func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
var i any
err := yaml.Unmarshal(data, &i)
if err != nil {
return E.FailWith("unmarshal yaml", err)
}
m, err := json.Marshal(i)
if err != nil {
return E.FailWith("marshal json", err)
}
err = schema.Validate(bytes.NewReader(m))
if err == nil {
return nil
}
errors := E.NewBuilder("yaml validation error")
for _, e := range err.(*jsonschema.ValidationError).Causes {
errors.AddE(e)
}
return errors.Build()
}
// 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.
// For a struct, it extracts the fields using the json tag if present, or the field name if not.
// For an embedded struct, it recursively converts its fields into the result map.
// For any other type, it returns an error.
//
// Parameters:
// - data: The data to be converted into a map.
//
// Returns:
// - result: The resulting map[string]any representation of the data.
// - error: An error if the data type is unsupported or if there is an error during conversion.
func Serialize(data any) (SerializedObject, E.NestedError) {
result := make(map[string]any)
// Use reflection to inspect the data type
value := reflect.ValueOf(data)
// Check if the value is valid
if !value.IsValid() {
return nil, E.Invalid("data", fmt.Sprintf("type: %T", data))
}
// Dereference pointers if necessary
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
// Handle different kinds of data
switch value.Kind() {
case reflect.Map:
for _, key := range value.MapKeys() {
result[key.String()] = value.MapIndex(key).Interface()
}
case reflect.Struct:
for i := 0; i < value.NumField(); i++ {
field := value.Type().Field(i)
if !field.IsExported() {
continue
}
jsonTag := field.Tag.Get("json") // Get the json tag
if jsonTag == "-" {
continue // Ignore this field if the tag is "-"
}
// If the json tag is not empty, use it as the key
if jsonTag != "" {
result[jsonTag] = value.Field(i).Interface()
} else if field.Anonymous {
// If the field is an embedded struct, add its fields to the result
fieldMap, err := Serialize(value.Field(i).Interface())
if err.HasError() {
return nil, err
}
for k, v := range fieldMap {
result[k] = v
}
} else {
result[field.Name] = value.Field(i).Interface()
}
}
default:
return nil, E.Unsupported("type", value.Kind())
}
return result, nil
}
func Deserialize(src SerializedObject, target any) E.NestedError {
// convert data fields to lower no-snake
// convert target fields to lower no-snake
// then check if the field of data is in the target
mapping := make(map[string]string)
t := reflect.TypeOf(target).Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
snakeCaseField := ToLowerNoSnake(field.Name)
mapping[snakeCaseField] = field.Name
}
tValue := reflect.ValueOf(target)
if tValue.IsZero() {
return E.Invalid("value", "nil")
}
for k, v := range src {
kCleaned := ToLowerNoSnake(k)
if fieldName, ok := mapping[kCleaned]; ok {
prop := reflect.ValueOf(target).Elem().FieldByName(fieldName)
propType := prop.Type()
isPtr := prop.Kind() == reflect.Ptr
if prop.CanSet() {
val := reflect.ValueOf(v)
vType := val.Type()
switch {
case isPtr && vType.ConvertibleTo(propType.Elem()):
ptr := reflect.New(propType.Elem())
ptr.Elem().Set(val.Convert(propType.Elem()))
prop.Set(ptr)
case vType.ConvertibleTo(propType):
prop.Set(val.Convert(propType))
case isPtr:
var vSerialized SerializedObject
vSerialized, ok = v.(SerializedObject)
if !ok {
if vType.ConvertibleTo(reflect.TypeFor[SerializedObject]()) {
vSerialized = val.Convert(reflect.TypeFor[SerializedObject]()).Interface().(SerializedObject)
} else {
return E.Failure(fmt.Sprintf("convert %s (%T) to %s", k, v, reflect.TypeFor[SerializedObject]()))
}
}
propNew := reflect.New(propType.Elem())
err := Deserialize(vSerialized, propNew.Interface())
if err.HasError() {
return E.Failure("set field").With(err).Subject(k)
}
prop.Set(propNew)
default:
return E.Invalid("conversion", k).Extraf("from %s to %s", vType, propType)
}
} else {
return E.Unsupported("field", k).Extraf("type %s is not settable", propType)
}
} else {
return E.Unexpected("field", k)
}
}
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 {
return strings.ToLower(strings.ReplaceAll(s, "_", ""))
}
type SerializedObject = map[string]any

23
internal/utils/string.go Normal file
View File

@@ -0,0 +1,23 @@
package utils
import (
"net/url"
"strconv"
"strings"
)
func CommaSeperatedList(s string) []string {
res := strings.Split(s, ",")
for i, part := range res {
res[i] = strings.TrimSpace(part)
}
return res
}
func ExtractPort(fullURL string) (int, error) {
url, err := url.Parse(fullURL)
if err != nil {
return 0, err
}
return strconv.Atoi(url.Port())
}

View File

@@ -0,0 +1,86 @@
package utils
import (
"errors"
"reflect"
"testing"
)
func ExpectNoError(t *testing.T, err error) {
t.Helper()
if err != nil && !reflect.ValueOf(err).IsNil() {
t.Errorf("expected err=nil, got %s", err.Error())
t.FailNow()
}
}
func ExpectError(t *testing.T, expected error, err error) {
t.Helper()
if !errors.Is(err, expected) {
t.Errorf("expected err %s, got %s", expected.Error(), err.Error())
t.FailNow()
}
}
func ExpectError2(t *testing.T, input any, expected error, err error) {
t.Helper()
if !errors.Is(err, expected) {
t.Errorf("%v: expected err %s, got %s", input, expected.Error(), err.Error())
t.FailNow()
}
}
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)
t.FailNow()
}
}
func ExpectEqualAny[T comparable](t *testing.T, got T, wants []T) {
t.Helper()
for _, want := range wants {
if got == want {
return
}
}
t.Errorf("expected any of:\n%v, got\n%v", wants, got)
t.FailNow()
}
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)
t.FailNow()
}
}
func ExpectTrue(t *testing.T, got bool) {
t.Helper()
if !got {
t.Error("expected true")
t.FailNow()
}
}
func ExpectFalse(t *testing.T, got bool) {
t.Helper()
if got {
t.Error("expected false")
t.FailNow()
}
}
func ExpectType[T any](t *testing.T, got any) (_ T) {
t.Helper()
tExpect := reflect.TypeFor[T]()
_, ok := got.(T)
if !ok {
t.Fatalf("expected type %s, got %s", tExpect, reflect.TypeOf(got).Elem())
t.FailNow()
return
}
return got.(T)
}