commit ebdf370c234b0a6f6234624868b684503d4314f3 Author: George Suntres Date: Sun Mar 29 10:32:25 2026 -0400 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a66ef27 --- /dev/null +++ b/.gitignore @@ -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 +!*/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..07534e1 --- /dev/null +++ b/Makefile @@ -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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..03cd6ba --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.gsuntres.com/general/commons + +go 1.25.0 + +require go.mongodb.org/mongo-driver/v2 v2.5.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..65a6c8c --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ce9c656 --- /dev/null +++ b/main.go @@ -0,0 +1,2 @@ +// Commons includes utility functions used across projects. +package commons diff --git a/math.go b/math.go new file mode 100644 index 0000000..e150b60 --- /dev/null +++ b/math.go @@ -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 +} diff --git a/math_test.go b/math_test.go new file mode 100644 index 0000000..97f0c3d --- /dev/null +++ b/math_test.go @@ -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 +} diff --git a/string_test.go b/string_test.go new file mode 100644 index 0000000..deac355 --- /dev/null +++ b/string_test.go @@ -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") + } +} diff --git a/strings.go b/strings.go new file mode 100644 index 0000000..50e8282 --- /dev/null +++ b/strings.go @@ -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) +} diff --git a/struct.go b/struct.go new file mode 100644 index 0000000..3378d27 --- /dev/null +++ b/struct.go @@ -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)) + } + } +} diff --git a/struct_test.go b/struct_test.go new file mode 100644 index 0000000..79124b4 --- /dev/null +++ b/struct_test.go @@ -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) + } + +}