initial commit. Cloned timetracker repository

This commit is contained in:
Daniel Goc
2026-03-10 09:02:57 +01:00
commit f2952bcef0
189 changed files with 21334 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
package mapper
// Package mapper provides utilities to map fields from one struct to another
// by matching field names (case-insensitive). Unmatched fields are left as
// their zero values.
import (
"fmt"
"reflect"
"strings"
)
// Map copies field values from src into dst by matching field names
// (case-insensitive). Fields in dst that have no counterpart in src
// are left at their zero value.
//
// Both dst and src must be pointers to structs.
// Returns an error if the types do not satisfy those constraints.
func Map(dst, src any) error {
dstVal := reflect.ValueOf(dst)
srcVal := reflect.ValueOf(src)
if dstVal.Kind() != reflect.Ptr || dstVal.Elem().Kind() != reflect.Struct {
return fmt.Errorf("mapper: dst must be a pointer to a struct, got %T", dst)
}
if srcVal.Kind() != reflect.Ptr || srcVal.Elem().Kind() != reflect.Struct {
return fmt.Errorf("mapper: src must be a pointer to a struct, got %T", src)
}
dstElem := dstVal.Elem()
srcElem := srcVal.Elem()
// Build a lookup map of src fields: lowercase name -> field value
srcFields := make(map[string]reflect.Value)
for i := 0; i < srcElem.NumField(); i++ {
f := srcElem.Type().Field(i)
if !f.IsExported() {
continue
}
srcFields[strings.ToLower(f.Name)] = srcElem.Field(i)
}
// Iterate over dst fields and copy matching src values
for i := 0; i < dstElem.NumField(); i++ {
dstField := dstElem.Field(i)
dstType := dstElem.Type().Field(i)
if !dstType.IsExported() || !dstField.CanSet() {
continue
}
srcField, ok := srcFields[strings.ToLower(dstType.Name)]
if !ok {
// No matching src field leave zero value in place
continue
}
if srcField.Type().AssignableTo(dstField.Type()) {
dstField.Set(srcField)
} else if srcField.Type().ConvertibleTo(dstField.Type()) {
dstField.Set(srcField.Convert(dstField.Type()))
}
// If neither assignable nor convertible, the dst field keeps its zero value
}
return nil
}
// MustMap is like Map but panics on error.
func MustMap(dst, src any) {
if err := Map(dst, src); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,78 @@
package mapper_test
import (
"testing"
"git.ma-al.com/goc_marek/timetracker/app/utils/mapper"
)
// --- example structs ---
type UserInput struct {
Name string
Email string
Age int
}
type UserRecord struct {
Name string
Email string
Age int
CreatedAt string // not in src → stays ""
Active bool // not in src → stays false
}
// --- tests ---
func TestMap_MatchingFields(t *testing.T) {
src := &UserInput{Name: "Alice", Email: "alice@example.com", Age: 30}
dst := &UserRecord{}
if err := mapper.Map(dst, src); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if dst.Name != "Alice" {
t.Errorf("Name: want Alice, got %s", dst.Name)
}
if dst.Email != "alice@example.com" {
t.Errorf("Email: want alice@example.com, got %s", dst.Email)
}
if dst.Age != 30 {
t.Errorf("Age: want 30, got %d", dst.Age)
}
}
func TestMap_UnmatchedFieldsAreZero(t *testing.T) {
src := &UserInput{Name: "Bob"}
dst := &UserRecord{}
mapper.MustMap(dst, src)
if dst.CreatedAt != "" {
t.Errorf("CreatedAt: expected empty string, got %q", dst.CreatedAt)
}
if dst.Active != false {
t.Errorf("Active: expected false, got %v", dst.Active)
}
}
func TestMap_TypeConversion(t *testing.T) {
type Src struct{ Score int32 }
type Dst struct{ Score int64 }
src := &Src{Score: 99}
dst := &Dst{}
mapper.MustMap(dst, src)
if dst.Score != 99 {
t.Errorf("Score: want 99, got %d", dst.Score)
}
}
func TestMap_InvalidInput(t *testing.T) {
if err := mapper.Map("not a struct", 42); err == nil {
t.Error("expected error for non-struct inputs")
}
}