From 2d8df259cbb814700f2b57a9afe51afde28ef2e8 Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Mon, 30 Jan 2023 17:40:50 +0100 Subject: [PATCH] feat: initial version --- .editorconfig | 27 +++++++++ .gitignore | 41 +++++++------ gapr.go | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++ gapr_test.go | 131 ++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + 5 files changed, 346 insertions(+), 20 deletions(-) create mode 100644 .editorconfig create mode 100644 gapr.go create mode 100644 gapr_test.go create mode 100644 go.mod diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..93166ef --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +tab_width = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +trim_trailing_whitespace = true + +[*.go] +indent_style = tab +ij_smart_tabs = true +ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true +ij_go_group_stdlib_imports = true +ij_go_import_sorting = goimports +ij_go_local_group_mode = project +ij_go_move_all_imports_in_one_declaration = true +ij_go_move_all_stdlib_imports_in_one_group = true +ij_go_remove_redundant_import_aliases = true + +[{*.yaml,*.yml}] +indent_size = 2 +tab_width = 2 +insert_final_newline = true diff --git a/.gitignore b/.gitignore index adf8f72..0341f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,24 @@ -# ---> Go -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +######### +# files # +######### -# Test binary, built with `go test -c` -*.test +**/cov.out +**/cov-raw.out +**/*.mock.go +*.key +*.pem +./main +.go/ +__debug_bin -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work +############### +# directories # +############### +.idea/ +.vscode/ +dist/ +out/ +.task/ +public/ +.fleet/ \ No newline at end of file diff --git a/gapr.go b/gapr.go new file mode 100644 index 0000000..b3fcc04 --- /dev/null +++ b/gapr.go @@ -0,0 +1,164 @@ +package gapr + +import ( + "errors" + "math/rand" + "reflect" + "strconv" + "strings" + "time" +) + +var ( + ErrNotSupportedType = errors.New("input is not a supported type") + ErrNotStringKey = errors.New("key type is not string - not supported right now") +) + +func New() *Gapr { + return &Gapr{ + rand: rand.New(rand.NewSource(time.Now().Unix())), + } +} + +type Gapr struct { + rand *rand.Rand +} + +func (g *Gapr) Map(input any) (any, error) { + t := reflect.TypeOf(input) + v := reflect.ValueOf(input) + + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + + if !canMap(t) { + return nil, ErrNotSupportedType + } + + switch t.Kind() { + case reflect.Map: + return g.mapMap(t, v) + case reflect.Struct, reflect.Interface: + return g.mapStruct(t, v) + case reflect.Slice, reflect.Array: + return g.mapSliceOrArray(t, v) + default: + return nil, ErrNotSupportedType + } +} + +func (g *Gapr) mapStruct(t reflect.Type, v reflect.Value) (any, error) { + numberOfFields := t.NumField() + mapped := make(map[string]any, numberOfFields) + + for i := 0; i < numberOfFields; i++ { + drop, fieldName, err := g.fieldMeta(t.Field(i)) + if err != nil { + return nil, err + } + + if drop { + continue + } + + if mappedVal, err := g.mapField(v.Field(i)); err != nil { + return nil, err + } else { + mapped[fieldName] = mappedVal + } + } + + return mapped, nil +} + +func (g *Gapr) mapField(v reflect.Value) (any, error) { + if canMap(v.Type()) { + return g.Map(v.Interface()) + } else { + return v.Interface(), nil + } +} + +func (g *Gapr) mapMap(t reflect.Type, v reflect.Value) (any, error) { + mapped := make(map[string]any, v.Len()) + + if t.Key().Kind() != reflect.String { + return nil, ErrNotStringKey + } + + iter := v.MapRange() + for iter.Next() { + if canMap(iter.Value().Type()) { + if mappedValue, err := g.Map(iter.Value().Interface()); err != nil { + return nil, err + } else { + mapped[iter.Key().String()] = mappedValue + } + } else { + mapped[iter.Key().String()] = iter.Value().Interface() + } + } + + return mapped, nil +} + +func (g *Gapr) mapSliceOrArray(t reflect.Type, v reflect.Value) (any, error) { + var ( + length = v.Len() + target = reflect.ValueOf(make([]any, length, length)) + ) + + for i := 0; i < length; i++ { + idxVal := v.Index(i) + if canMap(idxVal.Type()) { + if mappedVal, err := g.Map(idxVal.Interface()); err != nil { + return nil, err + } else { + target.Index(i).Set(reflect.ValueOf(mappedVal)) + } + } else { + target.Index(i).Set(idxVal) + } + } + + return target.Interface(), nil +} + +func (g *Gapr) fieldMeta(f reflect.StructField) (drop bool, fieldName string, err error) { + fieldName = f.Name + tagVal, present := f.Tag.Lookup("incomplete") + if !present { + return false, f.Name, nil + } + + tagSplit := strings.Split(tagVal, ",") + if len(tagSplit) < 2 { + return false, f.Name, nil + } + + dropProbability, err := strconv.ParseFloat(tagSplit[0], 64) + if err != nil { + return false, "", err + } + + drop = g.rand.Float64() < 1.0-dropProbability + if tagSplit[1] != "" { + fieldName = strings.TrimSpace(tagSplit[1]) + } + + return drop, fieldName, nil +} + +func canMap(t reflect.Type) bool { + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + + switch t.Kind() { + case reflect.Struct, reflect.Map, reflect.Interface, reflect.Slice, reflect.Array: + return true + default: + return false + } +} diff --git a/gapr_test.go b/gapr_test.go new file mode 100644 index 0000000..c81c9e4 --- /dev/null +++ b/gapr_test.go @@ -0,0 +1,131 @@ +package gapr_test + +import ( + "testing" + + "code.icb4dc0.de/prskr/gapr" +) + +func TestGapr_Map(t *testing.T) { + type args struct { + input any + } + tests := []struct { + name string + args args + want map[string]any + wantErr bool + }{ + { + name: "Test simple struct", + args: args{ + input: struct { + GivenName string + Surname string + }{ + GivenName: "Ted", + Surname: "Tester", + }, + }, + want: nil, + wantErr: false, + }, + { + name: "Test nested struct", + args: args{ + input: struct { + GivenName string + Surname string + Address struct { + Street string + HouseNr string + City string + } + }{ + GivenName: "Ted", + Surname: "Tester", + Address: struct { + Street string + HouseNr string + City string + }{ + Street: "Some Street", + HouseNr: "3a", + City: "Some City", + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "Test simple map", + args: args{ + input: struct { + GivenName string + Surname string + }{ + GivenName: "Ted", + Surname: "Tester", + }, + }, + want: nil, + wantErr: false, + }, + { + name: "Test nested slice", + args: args{ + input: struct { + GivenName string + Children []struct { + Name string + Age uint `incomplete:"0.2,givenName"` + } + }{ + GivenName: "Ted", + Children: []struct { + Name string + Age uint `incomplete:"0.2,givenName"` + }{ + { + Name: "Tim", + Age: 11, + }, + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "Test map with nested struct", + args: args{ + input: map[string]any{ + "Hello": struct { + GivenName string `incomplete:"0.2,givenName"` + Surname string + }{ + GivenName: "Ted", + Surname: "Tester", + }, + }, + }, + want: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := gapr.New() + got, err := g.Map(tt.args.input) + if (got == nil) == (err == nil) { + t.Errorf("cannot get nil value and nil error") + return + } + if (err != nil) != tt.wantErr { + t.Errorf("Map() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8fe752e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.icb4dc0.de/prskr/gapr + +go 1.18