commit ce541e342237e8bcb027455138550533a52bf6ce Author: admin Date: Thu Aug 7 16:23:09 2025 +0200 init diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b43505f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "cSpell.words": [ + "apihub", + "davecgh", + "difflib", + "gopkg", + "pmezard", + "stretchr", + "structmapper" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9deaeee --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2de2bb3 --- /dev/null +++ b/README.md @@ -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, +}) +``` diff --git a/api.go b/api.go new file mode 100644 index 0000000..896ff6d --- /dev/null +++ b/api.go @@ -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 +} diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000..3874ba4 --- /dev/null +++ b/api_test.go @@ -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)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5a6f866 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fce7fb8 --- /dev/null +++ b/go.sum @@ -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= diff --git a/makefile b/makefile new file mode 100644 index 0000000..b19eb1d --- /dev/null +++ b/makefile @@ -0,0 +1,4 @@ +install: + - go mod tidy +test: + - go test -timeout 30s .\... \ No newline at end of file diff --git a/registry.go b/registry.go new file mode 100644 index 0000000..6fda80a --- /dev/null +++ b/registry.go @@ -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) +} diff --git a/utils/type_name.go b/utils/type_name.go new file mode 100644 index 0000000..fce0b0f --- /dev/null +++ b/utils/type_name.go @@ -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 +} diff --git a/utils/type_name_test.go b/utils/type_name_test.go new file mode 100644 index 0000000..518078d --- /dev/null +++ b/utils/type_name_test.go @@ -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)) +}