Initial import
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Allowlisting gitignore template for GO projects prevents us
|
||||||
|
# from adding various unwanted local files, such as generated
|
||||||
|
# files, developer configurations or IDE-specific files etc.
|
||||||
|
#
|
||||||
|
# Recommended: Go.AllowList.gitignore
|
||||||
|
|
||||||
|
# Ignore everything
|
||||||
|
*
|
||||||
|
|
||||||
|
# But these files...
|
||||||
|
!.gitignore
|
||||||
|
|
||||||
|
!*.go
|
||||||
|
!go.sum
|
||||||
|
!go.mod
|
||||||
|
|
||||||
|
!README.md
|
||||||
|
!LICENSE
|
||||||
|
|
||||||
|
!Makefile
|
||||||
|
|
||||||
|
!*.sh
|
||||||
|
!*.md
|
||||||
|
|
||||||
|
# ...even if they are in subdirectories
|
||||||
|
!*/
|
||||||
40
Makefile
Normal file
40
Makefile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
.PHONY: help
|
||||||
|
|
||||||
|
help: ## This help.
|
||||||
|
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
GCFLAGS=
|
||||||
|
LDFLAGS_EXTRA=
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm coverage.out && true
|
||||||
|
|
||||||
|
lint:
|
||||||
|
go-critic check ./... || true
|
||||||
|
|
||||||
|
debug-test: ## Debug test with dlv
|
||||||
|
dlv test -- -test.v
|
||||||
|
|
||||||
|
connect:
|
||||||
|
dlv connect 127.0.0.1:36666
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test \
|
||||||
|
-failfast \
|
||||||
|
-race ./... -v
|
||||||
|
|
||||||
|
test-watch:
|
||||||
|
gotestsum \
|
||||||
|
--watch \
|
||||||
|
-- \
|
||||||
|
-failfast \
|
||||||
|
./...
|
||||||
|
|
||||||
|
cover:
|
||||||
|
go test -coverprofile=coverage.out
|
||||||
|
go tool cover -func=coverage.out
|
||||||
|
@rm coverage.out
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.gsuntres.com/general/commons
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||||
6
go.sum
Normal file
6
go.sum
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
2
main.go
Normal file
2
main.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Commons includes utility functions used across projects.
|
||||||
|
package commons
|
||||||
11
math.go
Normal file
11
math.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package commons
|
||||||
|
|
||||||
|
// MathMin will compare two integers and return the one with the smaller value.
|
||||||
|
func MathMin[T ~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||||
|
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
|
||||||
|
~float32 | ~float64 | ~string](a, b T) T {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
36
math_test.go
Normal file
36
math_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package commons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMathMin(t *testing.T) {
|
||||||
|
v := MathMin[int64](int64(15), int64(98))
|
||||||
|
|
||||||
|
if v != 15 {
|
||||||
|
t.Fatalf("Should have been 15 not %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMathMin_Swap(t *testing.T) {
|
||||||
|
v := MathMin[int64](int64(100), int64(12))
|
||||||
|
|
||||||
|
if v != 12 {
|
||||||
|
t.Fatalf("Should have been 12 not %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMathMin_String(t *testing.T) {
|
||||||
|
v := MathMin[string]("15", "152")
|
||||||
|
|
||||||
|
if v != "15" {
|
||||||
|
t.Fatalf("Should have been 15 not %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMathMin() {
|
||||||
|
fmt.Printf("%d", MathMin[int64](int64(10), int64(20)))
|
||||||
|
// Output:
|
||||||
|
// 10
|
||||||
|
}
|
||||||
27
string_test.go
Normal file
27
string_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package commons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringNormalize(t *testing.T) {
|
||||||
|
v := " some text here "
|
||||||
|
|
||||||
|
vv := StringNormalize(v)
|
||||||
|
|
||||||
|
if vv != "some text here" {
|
||||||
|
t.Fatal("Text should have been normalized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringIsBlank(t *testing.T) {
|
||||||
|
if !StringIsBlank("") {
|
||||||
|
t.Fatal("Failed to check string is blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringIsNotBlank(t *testing.T) {
|
||||||
|
if !StringIsNotBlank("fasdfdas") {
|
||||||
|
t.Fatal("Failed to check string is not blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
25
strings.go
Normal file
25
strings.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package commons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var whitespace = regexp.MustCompile(`\s+`)
|
||||||
|
|
||||||
|
// StringNormalize will replace arbitrary lengths of consecutive white space with a single one.
|
||||||
|
func StringNormalize(s string) string {
|
||||||
|
s = whitespace.ReplaceAllString(s, " ")
|
||||||
|
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
//StringIsBlank checks if a string is blank. Will trim the string before the check takes place.
|
||||||
|
func StringIsBlank(s string) bool {
|
||||||
|
return len(strings.TrimSpace(s)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringIsNotBlank check if a string is NOT blank.
|
||||||
|
func StringIsNotBlank(s string) bool {
|
||||||
|
return !StringIsBlank(s)
|
||||||
|
}
|
||||||
134
struct.go
Normal file
134
struct.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package commons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
// "encoding/json"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
// MapToStruct will convert a map[string]any to a struct.
|
||||||
|
func MapToStruct(m map[string]any, o any) error {
|
||||||
|
b, err := bson.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed marshal %v", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bson.Unmarshal(b, o)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to unmarshal %v", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapIsSubset given two map[string]any m1 and m2 will determine if m1 is a subset of m2.
|
||||||
|
// Only fields' name is evaluated not their values.
|
||||||
|
func MapIsSubset(subset, superset any) bool {
|
||||||
|
sub := subset.(map[string]any)
|
||||||
|
sup := superset.(map[string]any)
|
||||||
|
|
||||||
|
isSubset := true
|
||||||
|
|
||||||
|
for k, _ := range sub {
|
||||||
|
_, ok := sup[k]
|
||||||
|
if !ok {
|
||||||
|
isSubset = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSubset
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapIsSubsetOfStruct given a map[string]any and a struct will determine if the map is a subset of the struct.
|
||||||
|
// Only fields' name is evaluated not their values.
|
||||||
|
func MapIsSubsetOfStruct(m map[string]any, s any) bool {
|
||||||
|
v := reflect.ValueOf(s)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem() // Dereference if it's a pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we are working with a struct
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubset := true
|
||||||
|
|
||||||
|
t := v.Type()
|
||||||
|
for key := range m {
|
||||||
|
|
||||||
|
// FieldByName only finds exported fields
|
||||||
|
if _, found := t.FieldByName(key); !found {
|
||||||
|
isSubset = false
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSubset
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructHasJsonName determines if a struct has the given json tag name.
|
||||||
|
func StructHasJsonName(s any, targetName string) bool {
|
||||||
|
t := reflect.TypeOf(s)
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
tag := field.Tag.Get("json")
|
||||||
|
|
||||||
|
// The name is the first part before any commas (e.g., "user_id,omitempty")
|
||||||
|
name := strings.Split(tag, ",")[0]
|
||||||
|
|
||||||
|
if name == targetName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructHasJsonName determines if a struct has the given bson tag name.
|
||||||
|
func StructHasBsonName(s any, targetName string) bool {
|
||||||
|
t := reflect.TypeOf(s)
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
tag := field.Tag.Get("bson")
|
||||||
|
|
||||||
|
// The name is the first part before any commas (e.g., "user_id,omitempty")
|
||||||
|
name := strings.Split(tag, ",")[0]
|
||||||
|
|
||||||
|
if name == targetName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructCopyMatching copies the fields of one struct to another only if they have the same name and type.
|
||||||
|
func StructCopyMatching(source, target any) {
|
||||||
|
sVal := reflect.ValueOf(source).Elem()
|
||||||
|
tVal := reflect.ValueOf(target).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < sVal.NumField(); i++ {
|
||||||
|
sField := sVal.Type().Field(i)
|
||||||
|
tField, ok := tVal.Type().FieldByName(sField.Name)
|
||||||
|
|
||||||
|
if ok && sField.Type == tField.Type {
|
||||||
|
tVal.FieldByName(sField.Name).Set(sVal.Field(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
203
struct_test.go
Normal file
203
struct_test.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package commons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapToStruct(t *testing.T) {
|
||||||
|
o := map[string]any {
|
||||||
|
"name": "name1",
|
||||||
|
"age": 22,
|
||||||
|
"active": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var oo O
|
||||||
|
|
||||||
|
if err := MapToStruct(o, &oo); err != nil {
|
||||||
|
t.Fatalf("1 Failed to convert %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oo.Name != "name1" || oo.Age != 22 || !oo.Active {
|
||||||
|
t.Fatalf("2 Failed to convert %v", oo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapIsSubset(t *testing.T) {
|
||||||
|
super := map[string]any {
|
||||||
|
"name": "name1",
|
||||||
|
"age": 22,
|
||||||
|
"active": "2",
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := map[string]any {
|
||||||
|
"name": "a",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !MapIsSubset(sub, super) {
|
||||||
|
t.Fatal("Should have indicated that map is a subset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapIsSubset_Nosubset(t *testing.T) {
|
||||||
|
super := map[string]any {
|
||||||
|
"name": "name1",
|
||||||
|
"age": 22,
|
||||||
|
"active": "2",
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := map[string]any {
|
||||||
|
"foo": "a",
|
||||||
|
}
|
||||||
|
|
||||||
|
if MapIsSubset(sub, super) {
|
||||||
|
t.Fatal("Should have indicated that map is a NOT subset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapIsSubsetOfStruct_NoSubset(t *testing.T) {
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int `json:"age"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
oo := map[string]any{
|
||||||
|
"name": "A Name",
|
||||||
|
"age": 1,
|
||||||
|
"notes": "dsafda",
|
||||||
|
"other": "dd",
|
||||||
|
}
|
||||||
|
|
||||||
|
if MapIsSubsetOfStruct(oo, &O{}) {
|
||||||
|
t.Fatal("Should have indicated that map is a NOT subset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapIsSubsetOfStruct(t *testing.T) {
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int `json:"age"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
oo := map[string]any{
|
||||||
|
"Name": "A Name",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !MapIsSubsetOfStruct(oo, &O{}) {
|
||||||
|
t.Fatal("Should have indicated that map is a subset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapIsSubsetOfStruct_SomeFields(t *testing.T) {
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int `json:"age"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
oo := map[string]any{
|
||||||
|
"Name": "A Name",
|
||||||
|
"Foo": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
if MapIsSubsetOfStruct(oo, &O{}) {
|
||||||
|
t.Fatal("Should have indicated that map is a NOT subset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructHasJsonName_True(t *testing.T) {
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int `json:"age"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &O{
|
||||||
|
}
|
||||||
|
|
||||||
|
if !StructHasJsonName(o, "age") {
|
||||||
|
t.Fatal("False negative for json tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructHasJsonName_False(t *testing.T) {
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int `json:"age"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &O{
|
||||||
|
}
|
||||||
|
|
||||||
|
if StructHasJsonName(o, "foo") {
|
||||||
|
t.Fatal("False positive for json tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructHasBsonName_True(t *testing.T) {
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int `bson:"age"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &O{
|
||||||
|
}
|
||||||
|
|
||||||
|
if !StructHasBsonName(o, "age") {
|
||||||
|
t.Fatal("False negative for bson tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructHasBsonName_False(t *testing.T) {
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int `bson:"age"`
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &O{
|
||||||
|
}
|
||||||
|
|
||||||
|
if StructHasBsonName(o, "foo") {
|
||||||
|
t.Fatal("False positive for bson tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyMatching(t *testing.T) {
|
||||||
|
|
||||||
|
type O struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OO struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
from := &O{
|
||||||
|
Name: "Nick",
|
||||||
|
Age: 15,
|
||||||
|
Notes: "Some notes about nick",
|
||||||
|
}
|
||||||
|
|
||||||
|
var to OO
|
||||||
|
|
||||||
|
StructCopyMatching(from, &to)
|
||||||
|
|
||||||
|
if to.Name != "Nick" && to.Age != 15 && to.Active != true {
|
||||||
|
t.Fatalf("Failed to copy matching fields to %v", to)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user