├── CHANGELOG.md ├── README.md ├── consts └── consts.go ├── go.mod ├── go.sum ├── main.go └── mapper ├── mapper.go └── mapper_test.go /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.5 (2020-11-29) 2 | - Add invalid kinds handling 3 | 4 | ## 0.0.4 (2020-11-25) 5 | - Fix mapper tag bug 6 | - Add an interface underlying fields mapping 7 | 8 | ## 0.0.3 (2020-11-10) 9 | - Implement maps mapping 10 | - Fix tags mapping 11 | - Unit test base added 12 | 13 | ## 0.0.2 (2020-11-08) 14 | - Implement pointers mapping 15 | - Implement slices mapping 16 | - Rework initialisation process 17 | - Add documentation 18 | 19 | 20 | ## 0.0.1 (2020-11-06) 21 | - Add package base -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-mapper 2 | ## Installation 3 | `go get github.com/alexsem80/go-mapper` 4 | 5 | ## Usage 6 | 7 | ``` 8 | package main 9 | 10 | import ( 11 | "github.com/alexsem80/go-mapper/mapper" 12 | ) 13 | 14 | func main() { 15 | src := Destination{ 16 | Id: 1, 17 | Name: "Name", 18 | Weight: 32.32, 19 | Marks: []int32{2, 3, 4}, 20 | Address: []*NestedDestination{ 21 | { 22 | State: "RYA", 23 | Index: map[int]*DestinationMapType{ 24 | 1: {Name: "Name1"}, 25 | }, 26 | }, 27 | { 28 | State: "MOW", 29 | Index: map[int]*DestinationMapType{ 30 | 2: {Name: "Name2"}, 31 | }, 32 | }, 33 | }, 34 | } 35 | 36 | dest := &Source{} 37 | 38 | mapper := mapper.NewMapper() 39 | mapper.CreateMap((*Source)(nil), (*Destination)(nil)) 40 | mapper.CreateMap((*NestedSource)(nil), (*NestedDestination)(nil)) 41 | mapper.CreateMap((*SourceMapType)(nil), (*DestinationMapType)(nil)) 42 | 43 | mapper.Init() 44 | 45 | mapper.Map(dest, src) 46 | } 47 | 48 | type Source struct { 49 | ID int 50 | FirstName string `mapper:"Name"` 51 | Weight float32 52 | Marks []int32 53 | Address []*NestedSource 54 | } 55 | 56 | type NestedSource struct { 57 | State string 58 | Index map[int]*SourceMapType 59 | } 60 | 61 | type Destination struct { 62 | Id int `mapper:"ID"` 63 | Name string 64 | Weight float32 65 | Marks []int32 66 | Address []*NestedDestination 67 | } 68 | 69 | type NestedDestination struct { 70 | State string 71 | Index map[int]*DestinationMapType 72 | } 73 | 74 | type SourceMapType struct { 75 | Name string 76 | } 77 | 78 | type DestinationMapType struct { 79 | Name string 80 | } 81 | 82 | ``` 83 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const MapperTagName = "mapper" 4 | 5 | const ( 6 | SrcKeyIndex = iota 7 | DestKeyIndex 8 | ) 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alexsem80/go-mapper 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | github.com/stretchr/testify v1.6.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 4 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 9 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/alexsem80/go-mapper/mapper" 5 | ) 6 | 7 | func main() { 8 | src := Source{ 9 | ID: 1, 10 | Name: "Name", 11 | Weight: 32.32, 12 | Marks: []int32{2, 3, 4}, 13 | Address: []*NestedSource{ 14 | { 15 | State: "RYA", 16 | Index: map[int]SourceMapType{ 17 | 1: {Name: "Name1"}, 18 | }, 19 | }, 20 | { 21 | State: "MOW", 22 | Index: map[int]SourceMapType{ 23 | 2: {Name: "Name2"}, 24 | }, 25 | }, 26 | }, 27 | } 28 | 29 | dest := &Destination{} 30 | 31 | testMapper := mapper.NewMapper() 32 | testMapper.CreateMap((*Source)(nil), (*Destination)(nil)) 33 | testMapper.CreateMap((*NestedSource)(nil), (*NestedDestination)(nil)) 34 | testMapper.CreateMap((*SourceMapType)(nil), (*DestinationMapType)(nil)) 35 | testMapper.Init() 36 | 37 | testMapper.Map(src, dest) 38 | } 39 | 40 | type Source struct { 41 | ID int 42 | Name string 43 | Weight float32 44 | Marks []int32 45 | Address []*NestedSource 46 | } 47 | 48 | type NestedSource struct { 49 | State string 50 | Index map[int]SourceMapType 51 | } 52 | 53 | type Destination struct { 54 | Id int `mapper:"ID"` 55 | Name string 56 | Weight float32 57 | Marks []int32 58 | Address []*NestedDestination 59 | } 60 | 61 | type NestedDestination struct { 62 | State string 63 | Index map[int]DestinationMapType 64 | } 65 | 66 | type SourceMapType struct { 67 | Name string 68 | } 69 | 70 | type DestinationMapType struct { 71 | Name string 72 | } 73 | -------------------------------------------------------------------------------- /mapper/mapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/golang/glog" 9 | ) 10 | 11 | const ( 12 | SrcKeyIndex = iota 13 | DestKeyIndex 14 | ) 15 | 16 | // NewMapper func returns new uninitialised Mapper. 17 | // New maps should be created with CreateMap func. 18 | // After creating maps call Init to initialise Mapper. 19 | func NewMapper() *Mapper { 20 | return &Mapper{ 21 | isInitialised: false, 22 | profiles: make(map[string][][2]string), 23 | profilesOpts: make(map[string]*profileOptions), 24 | } 25 | } 26 | 27 | // Mapper struct contains maps for registered pairs of types 28 | // and profiles for resolving struct fields conversions. 29 | type Mapper struct { 30 | isInitialised bool // checks if Mapper was initialised before usage 31 | profiles map[string][][2]string // map of struct fields: ["srcType_destType"][]["src_key", "dest_key"] 32 | profilesOpts map[string]*profileOptions // options for profiles, such as reverse mapping, etc 33 | maps []map[reflect.Type]reflect.Type // pairs of types to map 34 | } 35 | 36 | // typeMeta struct contains meta info about struct fields 37 | // used to resolve conventions between fields names and tags. 38 | type typeMeta struct { 39 | keysToTags map[string]string 40 | tagsToKeys map[string]string 41 | } 42 | 43 | // profileOptions struct contains additional data for structs mapping. 44 | type profileOptions struct { 45 | reverseMap bool 46 | } 47 | 48 | // getProfileKey converts src and dest types in string key representation. 49 | func getProfileKey(srcType reflect.Type, destType reflect.Type) string { 50 | return fmt.Sprintf("%s_%s", srcType.Name(), destType.Name()) 51 | } 52 | 53 | // CreateMap func creates new spec for types mapping. 54 | // CreateMap should be called ONLY before Init function call. 55 | // Provided map can be reversed with chained Reverse function: 56 | // CreateMap((*Source)(nil), (*Destination)(nil)).Reverse() 57 | // You can create conversion between slices with MapSlices func 58 | // CreateMap((*Source)(nil), (*Destination)(nil)).MapSlices(). 59 | func (o *Mapper) CreateMap(src interface{}, dest interface{}) *Mapper { 60 | typesMap := make(map[reflect.Type]reflect.Type) 61 | typesMap[reflect.TypeOf(src).Elem()] = reflect.TypeOf(dest).Elem() 62 | 63 | o.maps = append(o.maps, typesMap) 64 | 65 | return o 66 | } 67 | 68 | // Reverse func reverts last created map. 69 | func (o *Mapper) Reverse() *Mapper { 70 | for srcType, destType := range o.maps[len(o.maps)-1] { 71 | o.profilesOpts[getProfileKey(srcType, destType)].reverseMap = true 72 | } 73 | 74 | return o 75 | } 76 | 77 | // Init func fills profiles from provided types maps. 78 | func (o *Mapper) Init() { 79 | // parse logger flags 80 | flag.Parse() 81 | 82 | for _, typesMap := range o.maps { 83 | for srcType, destType := range typesMap { 84 | // check for provided types kind. 85 | // if not struct - skip. 86 | if srcType.Kind() != reflect.Struct { 87 | glog.Errorf("expected reflect.Struct kind for type %s, but got %s", srcType.String(), srcType.Kind().String()) 88 | continue 89 | } 90 | 91 | if destType.Kind() != reflect.Struct { 92 | glog.Errorf("expected reflect.Struct kind for type %s, but got %s", destType.String(), destType.Kind().String()) 93 | continue 94 | } 95 | 96 | // if a reverse flag for given types exists add reverse map 97 | if options, ok := o.profilesOpts[getProfileKey(srcType, destType)]; ok && 98 | options.reverseMap { 99 | typesMap[destType] = srcType 100 | } 101 | 102 | // profile is slice of src and dest structs fields names 103 | var profile [][2]string 104 | 105 | // get types metadata 106 | srcMeta := o.getTypeMeta(srcType) 107 | destMeta := o.getTypeMeta(destType) 108 | 109 | for srcKey, srcTag := range srcMeta.keysToTags { 110 | // case src key equals dest key 111 | if _, ok := destMeta.keysToTags[srcKey]; ok { 112 | profile = append(profile, [2]string{srcKey, srcKey}) 113 | continue 114 | } 115 | 116 | // case src key equals dest tag 117 | if destKey, ok := destMeta.tagsToKeys[srcKey]; ok { 118 | profile = append(profile, [2]string{srcKey, destKey}) 119 | continue 120 | } 121 | 122 | // case src tag equals dest key 123 | if _, ok := destMeta.keysToTags[srcTag]; ok { 124 | profile = append(profile, [2]string{srcKey, srcTag}) 125 | continue 126 | } 127 | 128 | // case src tag equals dest tag 129 | if destKey, ok := destMeta.tagsToKeys[srcTag]; ok { 130 | profile = append(profile, [2]string{srcKey, destKey}) 131 | continue 132 | } 133 | } 134 | 135 | // save profile with unique srcKey for provided types 136 | o.profiles[getProfileKey(srcType, destType)] = profile 137 | } 138 | } 139 | 140 | o.isInitialised = true 141 | } 142 | 143 | // getTypeMeta func fetches struct fields keysToTags, types and Mapper tags. 144 | func (o *Mapper) getTypeMeta(val reflect.Type) typeMeta { 145 | fieldsNum := val.NumField() 146 | 147 | keysToTags := make(map[string]string) 148 | tagsToKeys := make(map[string]string) 149 | 150 | for i := 0; i < fieldsNum; i++ { 151 | field := val.Field(i) 152 | fieldName := field.Name 153 | fieldTag := field.Tag.Get("mapper") 154 | 155 | keysToTags[fieldName] = fieldTag 156 | tagsToKeys[fieldTag] = fieldName 157 | } 158 | 159 | return typeMeta{ 160 | keysToTags: keysToTags, 161 | tagsToKeys: tagsToKeys, 162 | } 163 | } 164 | 165 | // Map func checks for initialised Mapper and starts types mapping process. 166 | // Should be called ONLY after Init function call. 167 | func (o *Mapper) Map(src interface{}, dest interface{}) { 168 | // stop mapping if Mapper was not initialised 169 | if !o.isInitialised { 170 | glog.Error("uninitialised Mapper usage is permitted. You should call Init() func before Map() calling") 171 | return 172 | } 173 | 174 | // check if provided dest has pointer kind. 175 | destVal := reflect.ValueOf(dest) 176 | if destVal.Kind() != reflect.Ptr { 177 | glog.Errorf("provided destination has invalid kind: expected reflect.Ptr, got: %s", destVal.Kind().String()) 178 | return 179 | } 180 | 181 | // start values processing 182 | o.processValues(reflect.ValueOf(src), destVal.Elem()) 183 | } 184 | 185 | // processValues func resolve src and dest values kind 186 | // and either recursively calls mapping functions, or sets dest value. 187 | func (o *Mapper) processValues(src reflect.Value, dest reflect.Value) { 188 | // if src of dest is an interface - get underlying type 189 | if src.Kind() == reflect.Interface { 190 | src = src.Elem() 191 | } 192 | 193 | if dest.Kind() == reflect.Interface { 194 | dest = dest.Elem() 195 | } 196 | 197 | // get provided values' kinds 198 | srcKind := src.Kind() 199 | destKind := dest.Kind() 200 | 201 | // skip invalid kinds 202 | if srcKind == reflect.Invalid || destKind == reflect.Invalid { 203 | return 204 | } 205 | 206 | // check if kinds are equal 207 | if srcKind != destKind { 208 | // TODO dynamic cast, m.b. with Mapper extensions 209 | return 210 | } 211 | 212 | // if types are equal set dest value 213 | if src.Type() == dest.Type() { 214 | dest.Set(src) 215 | return 216 | } 217 | 218 | // resolve kind and choose mapping function 219 | // or set dest value 220 | switch src.Kind() { 221 | case reflect.Struct: 222 | o.mapStructs(src, dest) 223 | case reflect.Slice: 224 | o.mapSlices(src, dest) 225 | case reflect.Map: 226 | o.mapMaps(src, dest) 227 | case reflect.Ptr: 228 | o.mapPointers(src, dest) 229 | default: 230 | dest.Set(src) 231 | } 232 | } 233 | 234 | // mapStructs func perform structs casts. 235 | func (o *Mapper) mapStructs(src reflect.Value, dest reflect.Value) { 236 | // get values types 237 | // if types or their slices were not registered - abort 238 | profile, ok := o.profiles[getProfileKey(src.Type(), dest.Type())] 239 | if !ok { 240 | glog.Errorf("no conversion specified for types %s and %s", src.Type().String(), dest.Type().String()) 241 | return 242 | } 243 | 244 | // iterate over struct fields and map values 245 | for _, keys := range profile { 246 | o.processValues(src.FieldByName(keys[SrcKeyIndex]), dest.FieldByName(keys[DestKeyIndex])) 247 | } 248 | } 249 | 250 | // mapSlices func perform slices casts. 251 | func (o *Mapper) mapSlices(src reflect.Value, dest reflect.Value) { 252 | // Make dest slice 253 | dest.Set(reflect.MakeSlice(dest.Type(), src.Len(), src.Cap())) 254 | 255 | // Get each element of slice 256 | // process values mapping 257 | for i := 0; i < src.Len(); i++ { 258 | srcVal := src.Index(i) 259 | destVal := dest.Index(i) 260 | 261 | o.processValues(srcVal, destVal) 262 | } 263 | } 264 | 265 | // mapPointers func perform pointers casts. 266 | func (o *Mapper) mapPointers(src reflect.Value, dest reflect.Value) { 267 | // create new struct from provided dest type 268 | val := reflect.New(dest.Type().Elem()).Elem() 269 | 270 | o.processValues(src.Elem(), val) 271 | 272 | // assign address of initialised struct to destination 273 | dest.Set(val.Addr()) 274 | } 275 | 276 | // mapMaps func perform maps casts. 277 | func (o *Mapper) mapMaps(src reflect.Value, dest reflect.Value) { 278 | // Make dest map 279 | dest.Set(reflect.MakeMapWithSize(dest.Type(), src.Len())) 280 | 281 | // Get each element of map as key-values 282 | // process keys and values mapping and update dest map 283 | srcMapIter := src.MapRange() 284 | destMapIter := dest.MapRange() 285 | 286 | for destMapIter.Next() && srcMapIter.Next() { 287 | destKey := reflect.New(destMapIter.Key().Type()).Elem() 288 | destValue := reflect.New(destMapIter.Value().Type()).Elem() 289 | 290 | o.processValues(srcMapIter.Key(), destKey) 291 | o.processValues(srcMapIter.Value(), destValue) 292 | 293 | dest.SetMapIndex(destKey, destValue) 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /mapper/mapper_test.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var srcType = (*Source)(nil) 11 | var destType = (*Destination)(nil) 12 | 13 | var createMapTestData = []struct { 14 | label string 15 | srcType, destType interface{} 16 | reversed bool 17 | wantedMaps []map[reflect.Type]reflect.Type 18 | }{ 19 | { 20 | "Case-1: CreateMap Source -> Destination", 21 | srcType, 22 | destType, 23 | false, 24 | []map[reflect.Type]reflect.Type{ 25 | { 26 | reflect.TypeOf(srcType).Elem(): reflect.TypeOf(destType).Elem(), 27 | }, 28 | }, 29 | }, 30 | } 31 | 32 | func TestCreateMap(t *testing.T) { 33 | for _, data := range createMapTestData { 34 | testMapper := NewMapper() 35 | 36 | testMapper.CreateMap(data.srcType, data.destType) 37 | 38 | assert.True(t, reflect.DeepEqual(testMapper.maps, data.wantedMaps), data.label) 39 | } 40 | } 41 | 42 | type Source struct{} 43 | type Destination struct{} 44 | 45 | var mapTestData = []struct { 46 | label string 47 | srcObj, destObj interface{} 48 | }{ 49 | { 50 | "Case - 1. Map structs", 51 | SrcStruct{ 52 | ID: 1, 53 | Name: "Name", 54 | Weight: 10.5, 55 | Marks: []int32{1, 2, 3}, 56 | }, 57 | DestStruct{ 58 | Id: 1, 59 | Name: "Name", 60 | Weight: 10.5, 61 | Marks: []int32{1, 2, 3}, 62 | }, 63 | }, 64 | { 65 | "Case - 2. Map slices", 66 | struct { 67 | slice []SrcStruct 68 | }{ 69 | []SrcStruct{ 70 | { 71 | ID: 1, 72 | Name: "Name", 73 | Weight: 10.5, 74 | Marks: []int32{1, 2, 3}, 75 | }, 76 | { 77 | ID: 2, 78 | Name: "Name2", 79 | Weight: 20.5, 80 | Marks: []int32{3, 5, 7}, 81 | }, 82 | }, 83 | }, 84 | struct { 85 | slice []DestStruct 86 | }{ 87 | []DestStruct{ 88 | { 89 | Id: 1, 90 | Name: "Name", 91 | Weight: 10.5, 92 | Marks: []int32{1, 2, 3}, 93 | }, 94 | { 95 | Id: 2, 96 | Name: "Name2", 97 | Weight: 20.5, 98 | Marks: []int32{3, 5, 7}, 99 | }, 100 | }, 101 | }, 102 | }, 103 | } 104 | 105 | func TestMap(t *testing.T) { 106 | testMapper := NewMapper() 107 | testMapper.CreateMap((*SrcStruct)(nil), (*DestStruct)(nil)) 108 | testMapper.Init() 109 | 110 | for _, data := range mapTestData { 111 | dest := &struct{}{} 112 | testMapper.Map(data.srcObj, dest) 113 | assert.True(t, reflect.DeepEqual(reflect.ValueOf(dest).Elem(), data.destObj), data.label) 114 | } 115 | } 116 | 117 | type SrcStruct struct { 118 | ID int 119 | Name string 120 | Weight float32 121 | Marks []int32 122 | } 123 | 124 | type DestStruct struct { 125 | Id int `mapper:"ID"` 126 | Name string 127 | Weight float32 128 | Marks []int32 129 | } 130 | --------------------------------------------------------------------------------