mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-20 16:23:53 +01:00
preparing for v0.5
This commit is contained in:
50
src/utils/format.go
Normal file
50
src/utils/format.go
Normal file
@@ -0,0 +1,50 @@
|
||||
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 pluralize(n int64) string {
|
||||
if n > 1 {
|
||||
return "s"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
15
src/utils/fs.go
Normal file
15
src/utils/fs.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
func FileOK(p string) bool {
|
||||
_, err := os.Stat(p)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func FileName(p string) string {
|
||||
return path.Base(p)
|
||||
}
|
||||
69
src/utils/functional/functional.go
Normal file
69
src/utils/functional/functional.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package functional
|
||||
|
||||
import "sync"
|
||||
|
||||
func ForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) {
|
||||
for k := range obj {
|
||||
do(k)
|
||||
}
|
||||
}
|
||||
|
||||
func ForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) {
|
||||
for _, v := range obj {
|
||||
do(v)
|
||||
}
|
||||
}
|
||||
|
||||
func ForEachKV[K comparable, V interface{}](obj map[K]V, do func(K, V)) {
|
||||
for k, v := range obj {
|
||||
do(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func ParallelForEach[T interface{}](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 interface{}](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 interface{}](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 interface{}](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()
|
||||
}
|
||||
225
src/utils/functional/map.go
Normal file
225
src/utils/functional/map.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package functional
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type Map[KT comparable, VT interface{}] struct {
|
||||
m map[KT]VT
|
||||
defVals map[KT]VT
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// 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 interface{}](dv ...map[KT]VT) *Map[KT, VT] {
|
||||
return NewMapFrom(make(map[KT]VT), dv...)
|
||||
}
|
||||
|
||||
// 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 interface{}](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 interface{}](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] {
|
||||
if len(dv) > 0 {
|
||||
return &Map[KT, VT]{m: from, defVals: dv[0]}
|
||||
}
|
||||
return &Map[KT, VT]{m: from}
|
||||
}
|
||||
|
||||
func (m *Map[KT, VT]) Set(key KT, value VT) {
|
||||
m.Lock()
|
||||
m.m[key] = value
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}(v)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(result)
|
||||
}()
|
||||
|
||||
// The first valid match, if any
|
||||
select {
|
||||
case res, ok := <-result:
|
||||
if ok {
|
||||
return res
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// MergeWith merges the contents of another Map[KT, VT]
|
||||
// into the current Map[KT, VT] and
|
||||
// returns a map that were duplicated.
|
||||
//
|
||||
// Parameters:
|
||||
// - other: a pointer to another Map[KT, VT] to be merged into the current Map[KT, VT].
|
||||
//
|
||||
// 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)
|
||||
|
||||
m.Lock()
|
||||
for k, v := range other.m {
|
||||
if _, isDup := m.m[k]; !isDup {
|
||||
m.m[k] = v
|
||||
} else {
|
||||
dups[k] = v
|
||||
}
|
||||
}
|
||||
m.Unlock()
|
||||
return Map[KT, VT]{m: dups}
|
||||
}
|
||||
|
||||
func (m *Map[KT, VT]) Clear() {
|
||||
m.Lock()
|
||||
m.m = make(map[KT]VT)
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func (m *Map[KT, VT]) Size() int {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return len(m.m)
|
||||
}
|
||||
|
||||
func (m *Map[KT, VT]) Contains(key KT) bool {
|
||||
m.RLock()
|
||||
_, ok := m.m[key]
|
||||
m.RUnlock()
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func (m *Map[KT, VT]) Each(fn func(v VT)) {
|
||||
m.Lock()
|
||||
for _, v := range m.m {
|
||||
fn(v)
|
||||
}
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
69
src/utils/functional/slice.go
Normal file
69
src/utils/functional/slice.go
Normal file
@@ -0,0 +1,69 @@
|
||||
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] {
|
||||
return &Slice[T]{append(s.s, e)}
|
||||
}
|
||||
|
||||
func (s *Slice[T]) AddRange(other *Slice[T]) *Slice[T] {
|
||||
return &Slice[T]{append(s.s, other.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}
|
||||
}
|
||||
68
src/utils/functional/stringable.go
Normal file
68
src/utils/functional/stringable.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package functional
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Stringable struct{ string }
|
||||
|
||||
func NewStringable(v any) Stringable {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
return Stringable{vv}
|
||||
case fmt.Stringer:
|
||||
return Stringable{vv.String()}
|
||||
default:
|
||||
return Stringable{fmt.Sprint(vv)}
|
||||
}
|
||||
}
|
||||
|
||||
func (s Stringable) String() string {
|
||||
return s.string
|
||||
}
|
||||
|
||||
func (s Stringable) Len() int {
|
||||
return len(s.string)
|
||||
}
|
||||
|
||||
func (s Stringable) MarshalText() (text []byte, err error) {
|
||||
return []byte(s.string), nil
|
||||
}
|
||||
|
||||
func (s Stringable) SubStr(start int, end int) Stringable {
|
||||
return Stringable{s.string[start:end]}
|
||||
}
|
||||
|
||||
func (s Stringable) HasPrefix(p Stringable) bool {
|
||||
return len(s.string) >= len(p.string) && s.string[0:len(p.string)] == p.string
|
||||
}
|
||||
|
||||
func (s Stringable) HasSuffix(p Stringable) bool {
|
||||
return len(s.string) >= len(p.string) && s.string[len(s.string)-len(p.string):] == p.string
|
||||
}
|
||||
|
||||
func (s Stringable) IsEmpty() bool {
|
||||
return len(s.string) == 0
|
||||
}
|
||||
|
||||
func (s Stringable) IndexRune(r rune) int {
|
||||
return strings.IndexRune(s.string, r)
|
||||
}
|
||||
|
||||
func (s Stringable) ToInt() (int, error) {
|
||||
return strconv.Atoi(s.string)
|
||||
}
|
||||
|
||||
func (s Stringable) Split(sep string) []Stringable {
|
||||
return Stringables(strings.Split(s.string, sep))
|
||||
}
|
||||
|
||||
func Stringables(ss []string) []Stringable {
|
||||
ret := make([]Stringable, len(ss))
|
||||
for i, s := range ss {
|
||||
ret[i] = Stringable{s}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
152
src/utils/io.go
Normal file
152
src/utils/io.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type (
|
||||
Reader interface {
|
||||
Read() ([]byte, E.NestedError)
|
||||
}
|
||||
|
||||
StdReader struct {
|
||||
r Reader
|
||||
}
|
||||
|
||||
FileReader struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
ReadCloser struct {
|
||||
ctx context.Context
|
||||
r io.ReadCloser
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
StdReadCloser struct {
|
||||
r *ReadCloser
|
||||
}
|
||||
|
||||
ByteReader []byte
|
||||
NewByteReader = ByteReader
|
||||
|
||||
Pipe struct {
|
||||
r ReadCloser
|
||||
w io.WriteCloser
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
BidirectionalPipe struct {
|
||||
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) {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return 0, E.From(r.ctx.Err())
|
||||
default:
|
||||
return E.Check(r.r.Read(p))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReadCloser) Close() E.NestedError {
|
||||
if r.closed.Load() {
|
||||
return E.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()
|
||||
}
|
||||
|
||||
func NewPipe(ctx context.Context, r io.ReadCloser, w io.WriteCloser) *Pipe {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Pipe{
|
||||
r: ReadCloser{ctx: ctx, r: r},
|
||||
w: w,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pipe) Start() E.NestedError {
|
||||
return Copy(p.ctx, p.w, &StdReadCloser{&p.r})
|
||||
}
|
||||
|
||||
func (p *Pipe) Stop() E.NestedError {
|
||||
p.cancel()
|
||||
return E.Join("error stopping pipe", p.r.Close(), p.w.Close())
|
||||
}
|
||||
|
||||
func (p *Pipe) Write(b []byte) (int, E.NestedError) {
|
||||
return E.Check(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),
|
||||
}
|
||||
}
|
||||
|
||||
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() E.NestedError {
|
||||
errCh := make(chan E.NestedError, 2)
|
||||
go func() {
|
||||
errCh <- p.pSrcDst.Start()
|
||||
}()
|
||||
go func() {
|
||||
errCh <- p.pDstSrc.Start()
|
||||
}()
|
||||
for err := range errCh {
|
||||
if err.IsNotNil() {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (p *BidirectionalPipe) Stop() E.NestedError {
|
||||
return E.Join("error stopping pipe", p.pSrcDst.Stop(), p.pDstSrc.Stop())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
24
src/utils/reflection.go
Normal file
24
src/utils/reflection.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func snakeToPascal(s string) string {
|
||||
toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-"))
|
||||
return strings.ReplaceAll(toHyphenCamel, "-", "")
|
||||
}
|
||||
|
||||
func SetFieldFromSnake[T, VT any](obj *T, field string, value VT) E.NestedError {
|
||||
field = snakeToPascal(field)
|
||||
prop := reflect.ValueOf(obj).Elem().FieldByName(field)
|
||||
if prop.Kind() == 0 {
|
||||
return E.Invalid("field", field)
|
||||
}
|
||||
prop.Set(reflect.ValueOf(value))
|
||||
return E.Nil()
|
||||
}
|
||||
26
src/utils/schema.go
Normal file
26
src/utils/schema.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/santhosh-tekuri/jsonschema"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
)
|
||||
|
||||
var schemaCompiler = func() *jsonschema.Compiler {
|
||||
c := jsonschema.NewCompiler()
|
||||
c.Draft = jsonschema.Draft7
|
||||
return c
|
||||
}()
|
||||
|
||||
var schemaStorage = make(map[string] *jsonschema.Schema)
|
||||
|
||||
func GetSchema(path string) *jsonschema.Schema {
|
||||
if common.NoSchemaValidation {
|
||||
panic("bug: GetSchema called when schema validation disabled")
|
||||
}
|
||||
if schema, ok := schemaStorage[path]; ok {
|
||||
return schema
|
||||
}
|
||||
schema := schemaCompiler.MustCompile(path)
|
||||
schemaStorage[path] = schema
|
||||
return schema
|
||||
}
|
||||
127
src/utils/serialization.go
Normal file
127
src/utils/serialization.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/santhosh-tekuri/jsonschema"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
|
||||
var i interface{}
|
||||
|
||||
err := yaml.Unmarshal(data, &i)
|
||||
if err != nil {
|
||||
return E.Failure("unmarshal yaml").With(err)
|
||||
}
|
||||
|
||||
m, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return E.Failure("marshal json").With(err)
|
||||
}
|
||||
|
||||
err = schema.Validate(bytes.NewReader(m))
|
||||
if err == nil {
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("yaml validation error")
|
||||
for _, e := range err.(*jsonschema.ValidationError).Causes {
|
||||
errors.Add(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]interface{} 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]interface{} representation of the data.
|
||||
// - error: An error if the data type is unsupported or if there is an error during conversion.
|
||||
func Serialize(data interface{}) (SerializedObject, error) {
|
||||
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, fmt.Errorf("invalid 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 != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range fieldMap {
|
||||
result[k] = v
|
||||
}
|
||||
} else {
|
||||
result[field.Name] = value.Field(i).Interface()
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", value.Kind())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type SerializedObject map[string]any
|
||||
Reference in New Issue
Block a user