feat: initial version
This commit is contained in:
parent
5f026ea9b2
commit
2d8df259cb
5 changed files with 346 additions and 20 deletions
27
.editorconfig
Normal file
27
.editorconfig
Normal file
|
@ -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
|
41
.gitignore
vendored
41
.gitignore
vendored
|
@ -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/
|
164
gapr.go
Normal file
164
gapr.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
131
gapr_test.go
Normal file
131
gapr_test.go
Normal file
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module code.icb4dc0.de/prskr/gapr
|
||||
|
||||
go 1.18
|
Loading…
Reference in a new issue