This commit is contained in:
admin 2025-08-07 16:23:09 +02:00
commit ce541e3422
11 changed files with 647 additions and 0 deletions

11
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"cSpell.words": [
"apihub",
"davecgh",
"difflib",
"gopkg",
"pmezard",
"stretchr",
"structmapper"
]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Markus Morgenstern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

122
README.md Normal file
View File

@ -0,0 +1,122 @@
# Struct Mapper
The structmapper package converts Go structures into other Go structures. It is possible to use automatic conversion or to define your own strategies.
## AutoMap
AutoMap automatically attempts to convert Struct1 to Struct2. JSON serialisation is used for this purpose.
```Go
package main
import "git.apihub24.de/admin/structmapper"
type Struct1 struct {
Id int
Name string
}
type Struct2 struct {
Id int
Name string
Active bool
}
func main() {
result, err := structmapper.AutoMap[Struct1, Struct2](Struct1{
Id: 1,
Name: "test"
})
// prints 1
println(result.Id)
// prints test
println(result.Name)
// prints false
println(result.Active)
}
```
## Strategies
Custom mapping logics can be created and used with RegisterStrategy or removed again with UnregisterStrategy. These are then automatically searched for and used by the Map function. If no suitable strategy is found, an AutoMap is attempted.
```Go
type Right1 struct {
Id int
Name string
}
type Right2 struct {
Id int
Name string
}
structmapper.RegisterStrategy(func(from *Right1) (*Right2, error) {
return &testRight2{
Id: from.Id,
Name: from.Name,
}, nil
})
structmapper.RegisterStrategy(func(from *Right2) (*Right1, error) {
return &Right1{
Id: from.Id,
Name: from.Name,
}, nil
})
right := Right1 {
Id: 1,
Name: "test"
}
// now you can use the Map Method
result, err := structmapper.Map[*Right1, *Right2](right)
```
Strategies can be removed with UnregisterStrategy.
```Go
structmapper.UnregisterStrategy[Right1, Right2]()
structmapper.UnregisterStrategy[Right2, Right1]()
```
## SliceMap
SliceMap is a helper function that makes it easy to map slices in strategies. For complexity reasons, only an empty slice is returned here in the event of an error, not an error!
```Go
type Right1 struct {
Id int
Name string
}
type Right2 struct {
Id int
Name string
}
structmapper.RegisterStrategy(func(from *Right1) (*Right2, error) {
return &testRight2{
Id: from.Id,
Name: from.Name,
}, nil
})
structmapper.RegisterStrategy(func(from *Right2) (*Right1, error) {
return &Right1{
Id: from.Id,
Name: from.Name,
}, nil
})
right1 := Right1{
Id: 1,
Name: "READ"
}
right2 := Right1{
Id: 2,
Name: "WRITE"
}
result := structmapper.SliceMap[Right1, Right2]([]Right1{
right1, right2,
})
```

61
api.go Normal file
View File

@ -0,0 +1,61 @@
package structmapper
import (
"encoding/json"
"fmt"
di "git.apihub24.de/admin/generic-di"
"git.apihub24.de/admin/structmapper/utils"
)
func RegisterStrategy[TFrom any, TTo any](mapper func(TFrom) (TTo, error)) {
registry := di.Inject[*mapperRegistry]()
key := fmt.Sprintf("%s_%s", utils.GetName[TFrom](), utils.GetName[TTo]())
registry.RegisterStrategy(key, func(from any) (any, error) {
parsedFrom := from.(TFrom)
return mapper(parsedFrom)
})
}
func UnregisterStrategy[TFrom any, TTo any]() {
registry := di.Inject[*mapperRegistry]()
key := fmt.Sprintf("%s_%s", utils.GetName[TFrom](), utils.GetName[TTo]())
registry.UnregisterStrategy(key)
}
func Map[TFrom any, TTo any](from TFrom) (TTo, error) {
registry := di.Inject[*mapperRegistry]()
key := fmt.Sprintf("%s_%s", utils.GetName[TFrom](), utils.GetName[TTo]())
tmp, err := registry.Run(key, from)
if MappingStrategyNotFound.IsSameAs(err) {
return AutoMap[TFrom, TTo](from)
}
if err != nil {
var def TTo
return def, err
}
result := tmp.(TTo)
return result, err
}
func SliceMap[TFrom any, TTo any](from []TFrom) []TTo {
var err error
var rTo = make([]TTo, len(from))
for idx, rFrom := range from {
rTo[idx], err = Map[TFrom, TTo](rFrom)
if err != nil {
return make([]TTo, 0)
}
}
return rTo
}
func AutoMap[TFrom any, TTo any](from TFrom) (TTo, error) {
var result TTo
str, err := json.Marshal(from)
if err != nil {
return result, err
}
_ = json.Unmarshal(str, &result)
return result, nil
}

282
api_test.go Normal file
View File

@ -0,0 +1,282 @@
package structmapper_test
import (
"fmt"
"testing"
"git.apihub24.de/admin/structmapper"
"github.com/stretchr/testify/suite"
)
type testRight1 struct {
Id int
Name string
}
type testRight2 struct {
Id int
Name string
}
type testGroup1 struct {
Id int
Name string
Rights []*testRight1
}
type testGroup2 struct {
Id int
Name string
Rights []*testRight2
}
type testUser1 struct {
Id int
UserName string
Email string
Active bool
Groups []*testGroup1
}
type testUser2 struct {
Id int
UserName string
Email string
Active bool
Groups []*testGroup2
}
type ApiTestSuite struct {
suite.Suite
}
func (suite *ApiTestSuite) SetupSuite() {
// define Mapping for Right
structmapper.RegisterStrategy(func(from *testRight1) (*testRight2, error) {
return &testRight2{
Id: from.Id,
Name: from.Name,
}, nil
})
structmapper.RegisterStrategy(func(from *testRight2) (*testRight1, error) {
return &testRight1{
Id: from.Id,
Name: from.Name,
}, nil
})
// define Mapping for Group
structmapper.RegisterStrategy(func(from *testGroup1) (*testGroup2, error) {
return &testGroup2{
Id: from.Id,
Name: from.Name,
Rights: structmapper.SliceMap[*testRight1, *testRight2](from.Rights),
}, nil
})
structmapper.RegisterStrategy(func(from *testGroup2) (*testGroup1, error) {
return &testGroup1{
Id: from.Id,
Name: from.Name,
Rights: structmapper.SliceMap[*testRight2, *testRight1](from.Rights),
}, nil
})
// define Mapping for User
structmapper.RegisterStrategy(func(from *testUser1) (*testUser2, error) {
return &testUser2{
Id: from.Id,
UserName: from.UserName,
Email: from.Email,
Active: from.Active,
Groups: structmapper.SliceMap[*testGroup1, *testGroup2](from.Groups),
}, nil
})
structmapper.RegisterStrategy(func(from *testUser2) (*testUser1, error) {
return &testUser1{
Id: from.Id,
UserName: from.UserName,
Email: from.Email,
Active: from.Active,
Groups: structmapper.SliceMap[*testGroup2, *testGroup1](from.Groups),
}, nil
})
}
func (suite *ApiTestSuite) TestShouldMapFromTestRight1ToTestRight2() {
right := testRight1{
Id: 1,
Name: "READ",
}
mapped, err := structmapper.Map[*testRight1, *testRight2](&right)
suite.Nil(err)
suite.NotNil(mapped)
suite.Equal(mapped.Id, right.Id)
suite.Equal(mapped.Name, right.Name)
}
func (suite *ApiTestSuite) TestShouldMapFromTestRight2ToTestRight1() {
right := testRight2{
Id: 1,
Name: "READ",
}
mapped, err := structmapper.Map[*testRight2, *testRight1](&right)
suite.Nil(err)
suite.NotNil(mapped)
suite.Equal(mapped.Id, right.Id)
suite.Equal(mapped.Name, right.Name)
}
func (suite *ApiTestSuite) TestShouldMapFromGroup1ToGroup2() {
right := testRight1{
Id: 1,
Name: "READ",
}
group := testGroup1{
Id: 1,
Name: "admin",
Rights: []*testRight1{&right},
}
mapped, err := structmapper.Map[*testGroup1, *testGroup2](&group)
suite.Nil(err)
suite.NotNil(mapped)
suite.Equal(mapped.Id, group.Id)
suite.Equal(mapped.Name, group.Name)
suite.Equal(len(mapped.Rights), len(group.Rights))
}
func (suite *ApiTestSuite) TestShouldMapFromGroup2ToGroup1() {
right := testRight2{
Id: 1,
Name: "READ",
}
group := testGroup2{
Id: 1,
Name: "admin",
Rights: []*testRight2{&right},
}
mapped, err := structmapper.Map[*testGroup2, *testGroup1](&group)
suite.Nil(err)
suite.NotNil(mapped)
suite.Equal(mapped.Id, group.Id)
suite.Equal(mapped.Name, group.Name)
suite.Equal(len(mapped.Rights), len(group.Rights))
}
func (suite *ApiTestSuite) TestShouldMapFromUser1ToUser2() {
right := testRight1{
Id: 1,
Name: "READ",
}
group := testGroup1{
Id: 1,
Name: "admin",
Rights: []*testRight1{&right},
}
user := testUser1{
Id: 1,
UserName: "admin",
Email: "admin@example.de",
Active: true,
Groups: []*testGroup1{&group},
}
mapped, err := structmapper.Map[*testUser1, *testUser2](&user)
suite.Nil(err)
suite.NotNil(mapped)
suite.Equal(mapped.Id, user.Id)
suite.Equal(mapped.UserName, user.UserName)
suite.Equal(mapped.Email, user.Email)
suite.Equal(mapped.Active, user.Active)
suite.Equal(len(mapped.Groups), len(user.Groups))
}
func (suite *ApiTestSuite) TestShouldMapFromUser2ToUser1() {
right := testRight2{
Id: 1,
Name: "READ",
}
group := testGroup2{
Id: 1,
Name: "admin",
Rights: []*testRight2{&right},
}
user := testUser2{
Id: 1,
UserName: "admin",
Email: "admin@example.de",
Active: true,
Groups: []*testGroup2{&group},
}
mapped, err := structmapper.Map[*testUser2, *testUser1](&user)
suite.Nil(err)
suite.NotNil(mapped)
suite.Equal(mapped.Id, user.Id)
suite.Equal(mapped.UserName, user.UserName)
suite.Equal(mapped.Email, user.Email)
suite.Equal(mapped.Active, user.Active)
suite.Equal(len(mapped.Groups), len(user.Groups))
}
func (suite *ApiTestSuite) TestShouldUseAutoMapOnNoStrategy() {
right := testRight1{
Id: 1,
Name: "READ",
}
resultGroup, err := structmapper.Map[testRight1, testGroup1](right)
suite.False(structmapper.StrategyRuntimeException.IsSameAs(err))
suite.False(structmapper.MappingStrategyNotFound.IsSameAs(err))
suite.Equal(resultGroup.Id, right.Id)
suite.Equal(resultGroup.Name, right.Name)
result := structmapper.SliceMap[testRight1, testGroup1]([]testRight1{right})
suite.NotNil(result)
suite.Equal(len(result), 1)
suite.Equal(result[0].Id, right.Id)
suite.Equal(result[0].Name, right.Name)
}
func (suite *ApiTestSuite) TestShouldStrategyErrorsWasReturned() {
expectMessage := "StrategyError"
structmapper.RegisterStrategy(func(from testRight1) (testRight2, error) {
return testRight2{}, fmt.Errorf(expectMessage)
})
_, err := structmapper.Map[testRight1, testRight2](testRight1{
Id: 1,
Name: "READ",
})
suite.NotNil(err)
suite.True(structmapper.StrategyRuntimeException.IsSameAs(err))
}
func (suite *ApiTestSuite) TestShouldMapSerializeRight1ToRight2WithoutStrategy() {
right := testRight1{
Id: 1,
Name: "read",
}
result, err := structmapper.AutoMap[testRight1, testRight2](right)
suite.Nil(err)
suite.NotNil(result)
suite.Equal(right.Id, result.Id)
suite.Equal(right.Name, result.Name)
}
func (suite *ApiTestSuite) TestShouldMapSerializeDifferentStructs() {
right := testRight1{
Id: 1,
Name: "read",
}
result, err := structmapper.AutoMap[testRight1, testGroup1](right)
suite.Nil(err)
suite.NotNil(result)
}
func TestApiTestSuite(t *testing.T) {
suite.Run(t, new(ApiTestSuite))
}

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module git.apihub24.de/admin/structmapper
go 1.22.0
require (
git.apihub24.de/admin/exception v1.1.1
git.apihub24.de/admin/generic-di v1.4.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

16
go.sum Normal file
View File

@ -0,0 +1,16 @@
git.apihub24.de/admin/exception v1.1.1 h1:CGcH8PrJFZ6W35k6CEZgPrXzLX8oRSIwhzhjVTXIR8M=
git.apihub24.de/admin/exception v1.1.1/go.mod h1:PRlLjrJxdhSYAxYsUxJ6weRO8ARvIRjRvbcIIIJflDA=
git.apihub24.de/admin/generic-di v1.4.0 h1:0mQnpAcavMLBcnF5UO+tUI7abZ6zQPleqPsjEk3WIaU=
git.apihub24.de/admin/generic-di v1.4.0/go.mod h1:VcHV8MOb1qhwabHdO09CpjEg2VaDesehul86g1iyOxY=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

4
makefile Normal file
View File

@ -0,0 +1,4 @@
install:
- go mod tidy
test:
- go test -timeout 30s .\...

57
registry.go Normal file
View File

@ -0,0 +1,57 @@
package structmapper
import (
"fmt"
"sync"
exception "git.apihub24.de/admin/exception"
di "git.apihub24.de/admin/generic-di"
)
func init() {
di.Injectable(newMapperRegistry)
}
var (
MappingStrategyNotFound = exception.NewCustom("MappingStrategyNotFound")
StrategyRuntimeException = exception.NewCustom("StrategyRuntimeException")
)
func newMapperRegistry() *mapperRegistry {
return &mapperRegistry{
strategies: map[string]func(any) (any, error){},
mu: sync.Mutex{},
}
}
type mapperRegistry struct {
strategies map[string]func(any) (any, error)
mu sync.Mutex
}
func (registry *mapperRegistry) Run(key string, value any) (any, error) {
strategy, ok := registry.strategies[key]
if !ok {
return nil, MappingStrategyNotFound.With(fmt.Errorf("strategy key %s", key))
}
var err error
resultRef, err := strategy(value)
if err != nil {
return nil, StrategyRuntimeException.With(err)
}
return resultRef, nil
}
func (registry *mapperRegistry) RegisterStrategy(key string, strategy func(any) (any, error)) {
registry.mu.Lock()
defer registry.mu.Unlock()
registry.strategies[key] = strategy
}
func (registry *mapperRegistry) UnregisterStrategy(key string) {
registry.mu.Lock()
defer registry.mu.Unlock()
delete(registry.strategies, key)
}

15
utils/type_name.go Normal file
View File

@ -0,0 +1,15 @@
package utils
import "reflect"
func GetName[T any]() string {
var def T
typeName := ""
typeOf := reflect.TypeOf(def)
if typeOf != nil {
typeName = typeOf.String()
} else {
typeName = reflect.TypeOf((*T)(nil)).Elem().String()
}
return typeName
}

43
utils/type_name_test.go Normal file
View File

@ -0,0 +1,43 @@
package utils_test
import (
"testing"
"git.apihub24.de/admin/structmapper/utils"
"github.com/stretchr/testify/suite"
)
type testStruct struct{}
type testInterface interface{}
type UtilGetNameTestSuite struct {
suite.Suite
}
func (suite *UtilGetNameTestSuite) TestBaseTypeReturnsName() {
name := utils.GetName[int]()
suite.NotNil(name)
suite.Equal(name, "int")
}
func (suite *UtilGetNameTestSuite) TestStructReturnsName() {
name := utils.GetName[testStruct]()
suite.NotNil(name)
suite.Equal(name, "utils_test.testStruct")
}
func (suite *UtilGetNameTestSuite) TestStructReferenceReturnsName() {
name := utils.GetName[*testStruct]()
suite.NotNil(name)
suite.Equal(name, "*utils_test.testStruct")
}
func (suite *UtilGetNameTestSuite) TestInterfaceReturnsName() {
name := utils.GetName[testInterface]()
suite.NotNil(name)
suite.Equal(name, "utils_test.testInterface")
}
func TestUtilGetNameSuite(t *testing.T) {
suite.Run(t, new(UtilGetNameTestSuite))
}