diff --git a/.concourse/branch-validate.yml b/.concourse/branch-validate.yml new file mode 100644 index 0000000..4500611 --- /dev/null +++ b/.concourse/branch-validate.yml @@ -0,0 +1,42 @@ +--- +resources: + - name: gapr.git + type: git + icon: github + source: + uri: https://code.icb4dc0.de/prskr/gapr.git + branch: ((branch)) + + - name: templates.git + type: git + icon: github + source: + uri: https://code.icb4dc0.de/prskr/pipeline-templates.git + +jobs: + - name: lint + plan: + - get: gapr.git + trigger: true + - get: templates.git + - task: lint + file: gapr.git/.concourse/tasks/lint.yml + input_mapping: {repo: gapr.git} + on_success: + task: report-success + file: templates.git/tasks/gitea-status.yml + input_mapping: {repo: gapr.git} + vars: + project_path: prskr/gapr + context: concourse-ci/lint/golangci-lint + description: Lint Go files + state: success + on_failure: + task: report-failure + file: templates.git/tasks/gitea-status.yml + input_mapping: {repo: gapr.git} + vars: + project_path: prskr/gapr + context: concourse-ci/lint/golangci-lint + description: Lint Go files + state: failure diff --git a/.concourse/tasks/lint.yml b/.concourse/tasks/lint.yml new file mode 100644 index 0000000..964dba3 --- /dev/null +++ b/.concourse/tasks/lint.yml @@ -0,0 +1,24 @@ +--- +platform: linux + +image_resource: + type: registry-image + source: + repository: docker.io/golangci/golangci-lint + tag: latest + +inputs: + - name: repo + path: . + +params: + GO111MODULE: "on" + CGO_ENABLED: "0" + GITEA_TOKEN: ((gitea-credentials.token)) + +run: + path: bash + args: + - -ce + - | + golangci-lint run -v diff --git a/.concourse/tasks/test.yml b/.concourse/tasks/test.yml new file mode 100644 index 0000000..50467c9 --- /dev/null +++ b/.concourse/tasks/test.yml @@ -0,0 +1,24 @@ +--- +platform: linux + +image_resource: + type: registry-image + source: + repository: docker.io/golang + tag: 1.19-buster + +inputs: + - name: repo + path: . + +params: + GO111MODULE: "on" + CGO_ENABLED: "1" + GITEA_TOKEN: ((gitea-credentials.token)) + +run: + path: bash + args: + - -ce + - | + go run gotest.tools/gotestsum@latest ./... diff --git a/gapr.go b/gapr.go index e68e52e..44ea539 100644 --- a/gapr.go +++ b/gapr.go @@ -6,7 +6,6 @@ import ( "reflect" "strconv" "strings" - "time" ) var ( @@ -14,14 +13,22 @@ var ( ErrNotStringKey = errors.New("key type is not string - not supported right now") ) -func New() *Gapr { +func New(opts ...Option) *Gapr { + o := defaultOptions() + + for _, opt := range opts { + opt.apply(&o) + } + return &Gapr{ - rand: rand.New(rand.NewSource(time.Now().Unix())), + tagName: o.TagName, + rand: rand.New(rand.NewSource(o.RandomSeed)), } } type Gapr struct { - rand *rand.Rand + tagName string + rand *rand.Rand } func (g *Gapr) Map(input any) (any, error) { @@ -33,7 +40,7 @@ func (g *Gapr) Map(input any) (any, error) { v = v.Elem() } - if !canMap(t) { + if !canMap(t, v) { return nil, ErrNotSupportedType } @@ -74,7 +81,7 @@ func (g *Gapr) mapStruct(t reflect.Type, v reflect.Value) (any, error) { } func (g *Gapr) mapField(v reflect.Value) (any, error) { - if canMap(v.Type()) { + if canMap(v.Type(), v) { return g.Map(v.Interface()) } else { return v.Interface(), nil @@ -90,7 +97,7 @@ func (g *Gapr) mapMap(t reflect.Type, v reflect.Value) (any, error) { iter := v.MapRange() for iter.Next() { - if canMap(iter.Value().Type()) { + if canMap(iter.Value().Type(), iter.Value()) { if mappedValue, err := g.Map(iter.Value().Interface()); err != nil { return nil, err } else { @@ -112,7 +119,7 @@ func (g *Gapr) mapSliceOrArray(v reflect.Value) (any, error) { for i := 0; i < length; i++ { idxVal := v.Index(i) - if canMap(idxVal.Type()) { + if canMap(idxVal.Type(), idxVal) { if mappedVal, err := g.Map(idxVal.Interface()); err != nil { return nil, err } else { @@ -128,7 +135,7 @@ func (g *Gapr) mapSliceOrArray(v reflect.Value) (any, error) { func (g *Gapr) fieldMeta(f reflect.StructField) (drop bool, fieldName string, err error) { fieldName = f.Name - tagVal, present := f.Tag.Lookup("incomplete") + tagVal, present := f.Tag.Lookup(g.tagName) if !present { return false, f.Name, nil } @@ -138,12 +145,15 @@ func (g *Gapr) fieldMeta(f reflect.StructField) (drop bool, fieldName string, er return false, f.Name, nil } - dropProbability, err := strconv.ParseFloat(tagSplit[0], 64) - if err != nil { - return false, "", err + if tagSplit[0] != "" { + dropProbability, err := strconv.ParseFloat(tagSplit[0], 64) + if err != nil { + return false, "", err + } + + drop = g.rand.Float64() < dropProbability } - drop = g.rand.Float64() < dropProbability if len(tagSplit) > 1 && tagSplit[1] != "" { fieldName = strings.TrimSpace(tagSplit[1]) } @@ -151,13 +161,17 @@ func (g *Gapr) fieldMeta(f reflect.StructField) (drop bool, fieldName string, er return drop, fieldName, nil } -func canMap(t reflect.Type) bool { +func canMap(t reflect.Type, v reflect.Value) bool { if t.Kind() == reflect.Pointer { t = t.Elem() } + if t.Kind() == reflect.Interface { + t = reflect.TypeOf(v.Interface()) + } + switch t.Kind() { - case reflect.Struct, reflect.Map, reflect.Interface, reflect.Slice, reflect.Array: + case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array: return true default: return false diff --git a/gapr_test.go b/gapr_test.go index 623eeab..6408ee5 100644 --- a/gapr_test.go +++ b/gapr_test.go @@ -1,6 +1,7 @@ package gapr_test import ( + "reflect" "testing" "code.icb4dc0.de/prskr/gapr" @@ -9,11 +10,12 @@ import ( func TestGapr_Map(t *testing.T) { type args struct { input any + opts []gapr.Option } tests := []struct { name string args args - want map[string]any + want any wantErr bool }{ { @@ -27,7 +29,10 @@ func TestGapr_Map(t *testing.T) { Surname: "Tester", }, }, - want: nil, + want: map[string]any{ + "GivenName": "Ted", + "Surname": "Tester", + }, wantErr: false, }, { @@ -41,7 +46,10 @@ func TestGapr_Map(t *testing.T) { Surname: "Tester", }, }, - want: nil, + want: map[string]any{ + "GivenName": "Ted", + "Surname": "Tester", + }, wantErr: false, }, { @@ -69,21 +77,29 @@ func TestGapr_Map(t *testing.T) { }, }, }, - want: nil, + want: map[string]any{ + "GivenName": "Ted", + "Surname": "Tester", + "Address": map[string]any{ + "Street": "Some Street", + "HouseNr": "3a", + "City": "Some City", + }, + }, wantErr: false, }, { name: "Test simple map", args: args{ - input: struct { - GivenName string - Surname string - }{ - GivenName: "Ted", - Surname: "Tester", + input: map[string]any{ + "GivenName": "Ted", + "Surname": "Tester", }, }, - want: nil, + want: map[string]any{ + "GivenName": "Ted", + "Surname": "Tester", + }, wantErr: false, }, { @@ -93,13 +109,13 @@ func TestGapr_Map(t *testing.T) { GivenName string Children []struct { Name string - Age uint `incomplete:"0.2,givenName"` + Age uint `gapr:",age"` } }{ GivenName: "Ted", Children: []struct { Name string - Age uint `incomplete:"0.2,givenName"` + Age uint `gapr:",age"` }{ { Name: "Tim", @@ -108,7 +124,15 @@ func TestGapr_Map(t *testing.T) { }, }, }, - want: nil, + want: map[string]any{ + "GivenName": "Ted", + "Children": []any{ + map[string]any{ + "Name": "Tim", + "age": uint(11), + }, + }, + }, wantErr: false, }, { @@ -116,7 +140,7 @@ func TestGapr_Map(t *testing.T) { args: args{ input: map[string]any{ "Hello": struct { - GivenName string `incomplete:"0.2,givenName"` + GivenName string `gapr:",givenName"` Surname string }{ GivenName: "Ted", @@ -130,7 +154,7 @@ func TestGapr_Map(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - g := gapr.New() + g := gapr.New(tt.args.opts...) got, err := g.Map(tt.args.input) if (got == nil) == (err == nil) { t.Errorf("cannot get nil value and nil error") @@ -140,6 +164,10 @@ func TestGapr_Map(t *testing.T) { t.Errorf("Map() error = %v, wantErr %v", err, tt.wantErr) return } + + if tt.want != nil && !reflect.DeepEqual(got, tt.want) { + t.Errorf("Got %v but want %v", got, tt.want) + } }) } } diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..29375a1 --- /dev/null +++ b/keys.go @@ -0,0 +1,47 @@ +package gapr + +import ( + "strings" + "unicode" +) + +type KeyMapper interface { + MapKey(orig string) string +} + +type KeyMapperFunc func(orig string) string + +func (f KeyMapperFunc) MapKey(orig string) string { + return f(orig) +} + +var ( + LowercaseKeyMapper KeyMapper = KeyMapperFunc(func(orig string) string { + return strings.ToLower(orig) + }) + + UppercaseKeyMapper KeyMapper = KeyMapperFunc(func(orig string) string { + return strings.ToUpper(orig) + }) + + CamelCaseKeyMapper = genericFirstKeyMapper(unicode.ToLower) + + PascalCaseKeyMapper = genericFirstKeyMapper(unicode.ToUpper) +) + +func genericFirstKeyMapper(m func(r rune) rune) KeyMapper { + return KeyMapperFunc(func(orig string) string { + if len(orig) < 1 { + return "" + } + + mapped := make([]byte, 1, len(orig)) + mapped[0] = byte(m(rune(orig[0]))) + + if len(orig) > 1 { + mapped = append(mapped, orig[1:]...) + } + + return string(mapped) + }) +} diff --git a/keys_test.go b/keys_test.go new file mode 100644 index 0000000..a795ea2 --- /dev/null +++ b/keys_test.go @@ -0,0 +1,75 @@ +package gapr_test + +import ( + "testing" + + "code.icb4dc0.de/prskr/gapr" +) + +func TestKeyMappers(t *testing.T) { + t.Parallel() + tests := []struct { + name string + mapper gapr.KeyMapper + input string + want string + }{ + { + name: "Empty string", + mapper: gapr.CamelCaseKeyMapper, + input: "", + want: "", + }, + { + name: "HelloWorld camel case", + mapper: gapr.CamelCaseKeyMapper, + input: "HelloWorld", + want: "helloWorld", + }, + { + name: "helloWorld camel case", + mapper: gapr.CamelCaseKeyMapper, + input: "helloWorld", + want: "helloWorld", + }, + { + name: "h camel case", + mapper: gapr.CamelCaseKeyMapper, + input: "h", + want: "h", + }, + { + name: "HelloWorld lowercase", + mapper: gapr.LowercaseKeyMapper, + input: "HelloWorld", + want: "helloworld", + }, + { + name: "HelloWorld uppercase", + mapper: gapr.UppercaseKeyMapper, + input: "HelloWorld", + want: "HELLOWORLD", + }, + { + name: "HelloWorld pascal case", + mapper: gapr.PascalCaseKeyMapper, + input: "helloWorld", + want: "HelloWorld", + }, + { + name: "h pascal case", + mapper: gapr.PascalCaseKeyMapper, + input: "h", + want: "H", + }, + } + for _, tc := range tests { + tt := tc + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := tt.mapper.MapKey(tt.input); got != tt.want { + t.Errorf("Expected %s but got %s", tt.want, got) + } + }) + } +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..f67d9e2 --- /dev/null +++ b/options.go @@ -0,0 +1,37 @@ +package gapr + +import "time" + +func WithRandomSeed(i int64) Option { + return optionFunc(func(o *options) { + o.RandomSeed = i + }) +} + +func WithTagName(tag string) Option { + return optionFunc(func(o *options) { + o.TagName = tag + }) +} + +func defaultOptions() options { + return options{ + TagName: "gapr", + RandomSeed: time.Now().UTC().Unix(), + } +} + +type Option interface { + apply(o *options) +} + +type optionFunc func(o *options) + +func (f optionFunc) apply(o *options) { + f(o) +} + +type options struct { + TagName string + RandomSeed int64 +}