Repository: bxcodec/faker
Pull request #64 (2019-03-20 13:38:24)

Add support for keeping struct property values
Sometimes I want to keep the original value of a struct property. Therefore I added a tag `keep` which allows me to keep the initially set value. It still sets the value if it is the zero value of a field.


codecov[bot]: # [Codecov](https://codecov.io/gh/bxcodec/faker/pull/64?src=pr&el=h1) Report
> Merging [#64](https://codecov.io/gh/bxcodec/faker/pull/64?src=pr&el=desc) into [master](https://codecov.io/gh/bxcodec/faker/commit/e77d6ef38361639cfc728c880d5922a856c41959?src=pr&el=desc) will **decrease** coverage by `0.57%`.
> The diff coverage is `92.59%`.

[![Impacted file tree graph](https://codecov.io/gh/bxcodec/faker/pull/64/graphs/tree.svg?width=650&token=cLPGB7MHVc&height=150&src=pr)](https://codecov.io/gh/bxcodec/faker/pull/64?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #64      +/-   ##
==========================================
- Coverage   94.49%   93.91%   -0.58%     
==========================================
  Files          10       10              
  Lines         799      838      +39     
==========================================
+ Hits          755      787      +32     
- Misses         23       28       +5     
- Partials       21       23       +2
```


| [Impacted Files](https://codecov.io/gh/bxcodec/faker/pull/64?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [faker.go](https://codecov.io/gh/bxcodec/faker/pull/64/diff?src=pr&el=tree#diff-ZmFrZXIuZ28=) | `88.62% <92.59%> (-0.76%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/bxcodec/faker/pull/64?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/bxcodec/faker/pull/64?src=pr&el=footer). Last update [e77d6ef...00eedac](https://codecov.io/gh/bxcodec/faker/pull/64?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

sebastian-walter: @bxcodec Do you know when you have time to look at the PR?

Patch:
diff --git a/faker.go b/faker.go
index d9aef43..53a0b07 100644
--- a/faker.go
+++ b/faker.go
@@ -39,6 +39,7 @@ const (
 	letterIdxMax          = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
 	letterBytes           = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 	tagName               = "faker"
+	keep                  = "keep"
 	ID                    = "uuid_digit"
 	HyphenatedID          = "uuid_hyphenated"
 	EmailTag              = "email"
@@ -256,12 +257,12 @@ func FakeData(a interface{}) error {
 
 	rval := reflect.ValueOf(a)
 
-	finalValue, err := getValue(reflectType.Elem())
+	finalValue, err := getValue(a)
 	if err != nil {
 		return err
 	}
 
-	rval.Elem().Set(finalValue.Convert(reflectType.Elem()))
+	rval.Elem().Set(finalValue.Elem().Convert(reflectType.Elem()))
 	return nil
 }
 
@@ -318,15 +319,28 @@ func AddProvider(tag string, provider TaggedFunction) error {
 	return nil
 }
 
-func getValue(t reflect.Type) (reflect.Value, error) {
+func getValue(a interface{}) (reflect.Value, error) {
+	t := reflect.TypeOf(a)
+	if t == nil {
+		return reflect.Value{}, fmt.Errorf("interface{} not allowed")
+	}
 	k := t.Kind()
 
 	switch k {
 	case reflect.Ptr:
 		v := reflect.New(t.Elem())
-		val, err := getValue(t.Elem())
-		if err != nil {
-			return reflect.Value{}, err
+		var val reflect.Value
+		var err error
+		if a != reflect.Zero(reflect.TypeOf(a)).Interface() {
+			val, err = getValue(reflect.ValueOf(a).Elem().Interface())
+			if err != nil {
+				return reflect.Value{}, err
+			}
+		} else {
+			val, err = getValue(v.Elem().Interface())
+			if err != nil {
+				return reflect.Value{}, err
+			}
 		}
 		v.Elem().Set(val.Convert(t.Elem()))
 		return v, nil
@@ -342,20 +356,33 @@ func getValue(t reflect.Type) (reflect.Value, error) {
 				if !v.Field(i).CanSet() {
 					continue // to avoid panic to set on unexported field in struct
 				}
-				tag := t.Field(i).Tag.Get(tagName)
+				tags := decodeTags(t, i)
 
-				switch tag {
-				case "":
-					val, err := getValue(v.Field(i).Type())
+				switch {
+				case tags.keepOriginal:
+					zero, err := isZero(reflect.ValueOf(a).Field(i))
+					if err != nil {
+						return reflect.Value{}, err
+					}
+					if zero {
+						err := setDataWithTag(v.Field(i).Addr(), tags.fieldType)
+						if err != nil {
+							return reflect.Value{}, err
+						}
+						continue
+					}
+					v.Field(i).Set(reflect.ValueOf(a).Field(i))
+				case tags.fieldType == "":
+					val, err := getValue(v.Field(i).Interface())
 					if err != nil {
 						return reflect.Value{}, err
 					}
 					val = val.Convert(v.Field(i).Type())
 					v.Field(i).Set(val)
-				case SKIP:
+				case tags.fieldType == SKIP:
 					continue
 				default:
-					err := setDataWithTag(v.Field(i).Addr(), tag)
+					err := setDataWithTag(v.Field(i).Addr(), tags.fieldType)
 					if err != nil {
 						return reflect.Value{}, err
 					}
@@ -375,7 +402,7 @@ func getValue(t reflect.Type) (reflect.Value, error) {
 		}
 		v := reflect.MakeSlice(t, len, len)
 		for i := 0; i < v.Len(); i++ {
-			val, err := getValue(t.Elem())
+			val, err := getValue(v.Index(i).Interface())
 			if err != nil {
 				return reflect.Value{}, err
 			}
@@ -422,11 +449,14 @@ func getValue(t reflect.Type) (reflect.Value, error) {
 		}
 		v := reflect.MakeMap(t)
 		for i := 0; i < len; i++ {
-			key, err := getValue(t.Key())
+			keyInstance := reflect.New(t.Key()).Elem().Interface()
+			key, err := getValue(keyInstance)
 			if err != nil {
 				return reflect.Value{}, err
 			}
-			val, err := getValue(t.Elem())
+
+			valueInstance := reflect.New(t.Elem()).Elem().Interface()
+			val, err := getValue(valueInstance)
 			if err != nil {
 				return reflect.Value{}, err
 			}
@@ -440,6 +470,39 @@ func getValue(t reflect.Type) (reflect.Value, error) {
 
 }
 
+func isZero(field reflect.Value) (bool, error) {
+	for _, kind := range []reflect.Kind{reflect.Struct, reflect.Slice, reflect.Array, reflect.Map} {
+		if kind == field.Kind() {
+			return false, fmt.Errorf("keep not allowed on struct")
+		}
+	}
+	return reflect.Zero(field.Type()).Interface() == field.Interface(), nil
+}
+
+func decodeTags(typ reflect.Type, i int) structTag {
+	tags := strings.Split(typ.Field(i).Tag.Get(tagName), ",")
+
+	keepOriginal := false
+	res := make([]string, 0)
+	for _, tag := range tags {
+		if tag == keep {
+			keepOriginal = true
+			continue
+		}
+		res = append(res, tag)
+	}
+
+	return structTag{
+		fieldType:    strings.Join(res, ","),
+		keepOriginal: keepOriginal,
+	}
+}
+
+type structTag struct {
+	fieldType    string
+	keepOriginal bool
+}
+
 func setDataWithTag(v reflect.Value, tag string) error {
 
 	if v.Kind() != reflect.Ptr {


Test patch:
diff --git a/faker_test.go b/faker_test.go
index 56baf75..a4c46bc 100644
--- a/faker_test.go
+++ b/faker_test.go
@@ -297,9 +297,9 @@ func TestUnsuportedMapStringInterface(t *testing.T) {
 	type Sample struct {
 		Map map[string]interface{}
 	}
-	sample := new(Sample)
+	var sample = new(Sample)
 	if err := FakeData(sample); err == nil {
-		t.Error("Expected Got Error. But got nil")
+		t.Error("Expected Error. But got nil")
 	}
 }
 
@@ -819,3 +819,97 @@ func TestTagWithPointer(t *testing.T) {
 		t.Error("Expected filled but got emtpy")
 	}
 }
+
+func TestItOverwritesDefaultValueIfKeepIsSet(t *testing.T) {
+	type TestStruct struct {
+		Email     string `json:"email,omitempty" faker:"email,keep"`
+	}
+
+	test := TestStruct{}
+
+	err := FakeData(&test)
+	if err != nil {
+		t.Error("expected not error, but got: ", err)
+	}
+
+	if test.Email == "" {
+		t.Error("expected filled but got empty")
+	}
+}
+func TestItKeepsStructPropertyWhenTagKeepIsSet(t *testing.T) {
+	type TestStruct struct {
+		FirstName string `json:"first_name,omitempty" faker:"first_name_male,keep"`
+		Email     string `json:"email,omitempty" faker:"email,keep"`
+	}
+
+	firstName := "Heino van der Laien"
+	test := TestStruct{
+		FirstName: firstName,
+	}
+
+	err := FakeData(&test)
+	if err != nil {
+		t.Error("expected not error, but got: ", err)
+	}
+
+	if test.FirstName != firstName {
+		t.Fatalf("expected: %s, but got: %s", firstName, test.FirstName)
+	}
+
+	if test.Email == "" {
+		t.Error("expected filled but got empty")
+	}
+}
+
+func TestItThrowsAnErrorWhenKeepIsUsedOnIncomparableType(t *testing.T) {
+	type TypeStructWithStruct struct {
+		Struct struct{} `faker:"first_name_male,keep"`
+	}
+	type TypeStructWithMap struct {
+		Map map[string]string `faker:"first_name_male,keep"`
+	}
+	type TypeStructWithSlice struct {
+		Slice []string `faker:"first_name_male,keep"`
+	}
+	type TypeStructWithArray struct {
+		Array [4]string `faker:"first_name_male,keep"`
+	}
+
+	withStruct := TypeStructWithStruct{}
+	withMap := TypeStructWithMap{}
+	withSlice := TypeStructWithSlice{}
+	withArray := TypeStructWithArray{}
+
+	for _, item := range []interface{}{withArray,withStruct,withMap,withSlice} {
+		err := FakeData(&item)
+		if err == nil {
+			t.Errorf("expected error, but got nil")
+		}
+	}
+}
+
+func TestItThrowsAnErrorWhenPointerToInterfaceIsUsed(t *testing.T) {
+	type PtrToInterface struct {
+		Interface *interface{}
+	}
+
+	interfacePtr := PtrToInterface{}
+
+	err := FakeData(&interfacePtr)
+	if err == nil {
+		t.Errorf("expected error, but got nil")
+	}
+}
+
+func TestItThrowsAnErrorWhenZeroValueWithKeepAndUnsupportedTagIsUsed(t *testing.T) {
+	type String struct {
+		StringVal string `faker:"keep,unsupported"`
+	}
+
+	val := String{}
+
+	err := FakeData(&val)
+	if err == nil {
+		t.Errorf("expected error, but got nil")
+	}
+}