Repository: bxcodec/faker
Pull request #103 (2020-06-07 02:33:30)

feat: Implement oneof tag
- This adds support for a `faker:"oneof: val1 :val2 :val3"`
- Currently only `string` and `int` is suppported
- I would be happy to add support for more types at a later time
- This looks like it might fix (or at least partially fix) https://github.com/bxcodec/faker/issues/82

codecov[bot]: # [Codecov](https://codecov.io/gh/bxcodec/faker/pull/103?src=pr&el=h1) Report
> Merging [#103](https://codecov.io/gh/bxcodec/faker/pull/103?src=pr&el=desc) into [master](https://codecov.io/gh/bxcodec/faker/commit/627121e0bd014927f980e231ba34eebc41d9cf18&el=desc) will **increase** coverage by `0.09%`.
> The diff coverage is `93.93%`.

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

```diff
@@            Coverage Diff             @@
##           master     #103      +/-   ##
==========================================
+ Coverage   94.90%   95.00%   +0.09%     
==========================================
  Files          10       10              
  Lines        1001     1020      +19     
==========================================
+ Hits          950      969      +19     
  Misses         28       28              
  Partials       23       23              
```


| [Impacted Files](https://codecov.io/gh/bxcodec/faker/pull/103?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [faker.go](https://codecov.io/gh/bxcodec/faker/pull/103/diff?src=pr&el=tree#diff-ZmFrZXIuZ28=) | `91.04% <93.93%> (+0.36%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/bxcodec/faker/pull/103?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/103?src=pr&el=footer). Last update [627121e...c35890a](https://codecov.io/gh/bxcodec/faker/pull/103?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

bxcodec: Can you resolve the conflict, please?

I can't merge to your PR
andrew-werdna: oh  yes of course! my apologies, I didn't notice that.
andrew-werdna: @bxcodec my apologies, I didn't notice the conflicts somehow. I just caught my branch up with your master, and fixed the merge conflicts!
andrew-werdna: I'm down for whenever/however I can help. I'm thinking I'll probably contribute the most on weekends. I'm happy to coordinate with you at your convenience. I can wait, or jump in about for `float` this weekend. Or look at other issues too in the coming weekends. Just let me know.
bxcodec: Thanks bro, float will be great as well. But take your time, no rush, I'm in debt for your help 🙏 .

I'm merging this now

Patch:
diff --git a/README.md b/README.md
index 1fd9682..ea35660 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Faker will generate you a fake data based on your Struct.
 [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/bxcodec/faker/blob/master/LICENSE)
 [![GoDoc](https://godoc.org/github.com/bxcodec/faker?status.svg)](https://godoc.org/github.com/bxcodec/faker)
 [![Go.Dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/bxcodec/faker/v3?tab=doc)
- 
+
 ## Index
 
 * [Support](#support)
@@ -38,8 +38,8 @@ go get -u github.com/bxcodec/faker/v3
 # Example
 
 ---
- 
- - Using Struct's tag: 
+
+ - Using Struct's tag:
    - [basic tags: example_with_tags_test.go](/example_with_tags_test.go)
    - [length and bounds: example_with_tags_lenbounds_test.go](/example_with_tags_lenbounds_test.go)
    - [language: example_with_tags_lang_test.go](/example_with_tags_lang_test.go)
@@ -47,7 +47,7 @@ go get -u github.com/bxcodec/faker/v3
  - Custom Struct's tag (define your own faker data): [example_custom_faker_test.go](/example_custom_faker_test.go)
  - Without struct's tag: [example_without_tag_test.go](/example_without_tag_test.go)
  - Single Fake Data Function: [example_single_fake_data_test.go](/example_single_fake_data_test.go)
- 
+
 ## DEMO
 
 ---
@@ -94,6 +94,7 @@ Unfortunately this library has some limitation
 * It does not support the `map[interface{}]interface{}`, `map[any_type]interface{}` & `map[interface{}]any_type` data types. Once again, we cannot generate values for an unknown data type.
 * Custom types are not fully supported. However some custom types are already supported: we are still investigating how to do this the correct way. For now, if you use `faker`, it's safer not to use any custom types in order to avoid panics.
 * Some extra custom types can be supported IF AND ONLY IF extended with [AddProvider()](https://github.com/bxcodec/faker/blob/9169c33ae9926e5b8f8732909790ee20b10b736a/faker.go#L320) please see [example](example_custom_faker_test.go#L46)
+* The `oneof` tag currently only supports `string` & `int`. Further support is coming soon. See [example](example_with_tags_test.go#L53) for usage.
 
 ## Contribution
 
diff --git a/faker.go b/faker.go
index 2f27ea5..0e00bcf 100644
--- a/faker.go
+++ b/faker.go
@@ -113,6 +113,9 @@ const (
 	BoundaryEnd           = "boundary_end"
 	Equals                = "="
 	comma                 = ","
+	colon                 = ":"
+	ONEOF                 = "oneof"
+	// period                = "."
 )
 
 var defaultTag = map[string]string{
@@ -233,6 +236,9 @@ var (
 	ErrWrongFormattedTag       = "Tag \"%s\" is not written properly"
 	ErrUnknownType             = "Unknown Type"
 	ErrNotSupportedTypeForTag  = "Type is not supported by tag."
+	ErrUnsupportedTagArguments = "Tag arguments are not compatible with field type."
+	ErrDuplicateSeparator      = "Duplicate separator for tag arguments."
+	ErrNotEnoughTagArguments   = "Not enough arguments for tag."
 )
 
 // Compiled regexp
@@ -636,21 +642,6 @@ func setDataWithTag(v reflect.Value, tag string) error {
 		reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
 		return userDefinedNumber(v, tag)
 	case reflect.Slice, reflect.Array:
-		/**
-		 * check for added Provider tag first before
-		 * defaulting to userDefinedArray()
-		 * this way the user at least has the
-		 * option of a custom tag working
-		 */
-		_, tagExists := mapperTag[tag]
-		if tagExists {
-			res, err := mapperTag[tag](v)
-			if err != nil {
-				return err
-			}
-			v.Set(reflect.ValueOf(res))
-			return nil
-		}
 		return userDefinedArray(v, tag)
 	case reflect.Map:
 		return userDefinedMap(v, tag)
@@ -720,6 +711,15 @@ func getValueWithTag(t reflect.Type, tag string) (interface{}, error) {
 }
 
 func userDefinedArray(v reflect.Value, tag string) error {
+	_, tagExists := mapperTag[tag]
+	if tagExists {
+		res, err := mapperTag[tag](v)
+		if err != nil {
+			return err
+		}
+		v.Set(reflect.ValueOf(res))
+		return nil
+	}
 	len := randomSliceAndMapSize()
 	if shouldSetNil && len == 0 {
 		v.Set(reflect.Zero(v.Type()))
@@ -787,7 +787,8 @@ func extractStringFromTag(tag string) (interface{}, error) {
 	var err error
 	strlen := randomStringLen
 	strlng := &lang
-	if !strings.Contains(tag, Length) && !strings.Contains(tag, Language) {
+	isOneOfTag := strings.Contains(tag, ONEOF)
+	if !strings.Contains(tag, Length) && !strings.Contains(tag, Language) && !isOneOfTag {
 		return nil, fmt.Errorf(ErrTagNotSupported, tag)
 	}
 	if strings.Contains(tag, Length) {
@@ -803,6 +804,22 @@ func extractStringFromTag(tag string) (interface{}, error) {
 			return nil, fmt.Errorf(ErrWrongFormattedTag, tag)
 		}
 	}
+	if isOneOfTag {
+		items := strings.Split(tag, colon)
+		argsList := items[1:]
+		if len(argsList) != 1 {
+			return nil, fmt.Errorf(ErrUnsupportedTagArguments)
+		}
+		if strings.Contains(argsList[0], ",,") {
+			return nil, fmt.Errorf(ErrDuplicateSeparator)
+		}
+		args := strings.Split(argsList[0], comma)
+		if len(args) < 2 {
+			return nil, fmt.Errorf(ErrNotEnoughTagArguments)
+		}
+		toRet := args[rand.Intn(len(args))]
+		return strings.TrimSpace(toRet), nil
+	}
 	res := randomString(strlen, strlng)
 	return res, nil
 }
@@ -826,9 +843,42 @@ func extractLangFromTag(tag string) (*langRuneBoundary, error) {
 }
 
 func extractNumberFromTag(tag string, t reflect.Type) (interface{}, error) {
-	if !strings.Contains(tag, BoundaryStart) || !strings.Contains(tag, BoundaryEnd) {
+	hasOneOf := strings.Contains(tag, ONEOF)
+	hasBoundaryStart := strings.Contains(tag, BoundaryStart)
+	hasBoundaryEnd := strings.Contains(tag, BoundaryEnd)
+	usingOneOfTag := hasOneOf && (!hasBoundaryStart && !hasBoundaryEnd)
+	usingBoundariesTags := !hasOneOf && (hasBoundaryStart && hasBoundaryEnd)
+	if !usingOneOfTag && !usingBoundariesTags {
 		return nil, fmt.Errorf(ErrTagNotSupported, tag)
 	}
+
+	// handling oneof tag
+	if usingOneOfTag {
+		argsList := strings.Split(tag, colon)[1:]
+		if len(argsList) != 1 {
+			return nil, fmt.Errorf(ErrUnsupportedTagArguments)
+		}
+		if strings.Contains(argsList[0], ",,") {
+			return nil, fmt.Errorf(ErrDuplicateSeparator)
+		}
+		args := strings.Split(argsList[0], comma)
+		if len(args) < 2 {
+			return nil, fmt.Errorf(ErrNotEnoughTagArguments)
+		}
+		var numberValues []int
+		for _, i := range args {
+			k := strings.TrimSpace(i)
+			j, err := strconv.Atoi(k)
+			if err != nil {
+				return nil, fmt.Errorf(ErrUnsupportedTagArguments)
+			}
+			numberValues = append(numberValues, j)
+		}
+		toRet := numberValues[rand.Intn(len(numberValues))]
+		return toRet, nil
+	}
+
+	// handling boundary tags
 	valuesStr := strings.SplitN(tag, comma, -1)
 	if len(valuesStr) != 2 {
 		return nil, fmt.Errorf(ErrWrongFormattedTag, tag)


Test patch:
diff --git a/example_custom_faker_test.go b/example_custom_faker_test.go
index dda4a9a..77e94f2 100644
--- a/example_custom_faker_test.go
+++ b/example_custom_faker_test.go
@@ -42,7 +42,7 @@ func CustomGenerator() {
 	})
 
 	_ = faker.AddProvider("customUUID", func(v reflect.Value) (interface{}, error) {
-		s := []byte{
+		s := CustomUUID{
 			0, 8, 7, 2, 3,
 		}
 		return s, nil
diff --git a/example_with_tags_test.go b/example_with_tags_test.go
index 4dd08a0..e392390 100644
--- a/example_with_tags_test.go
+++ b/example_with_tags_test.go
@@ -50,6 +50,8 @@ type SomeStructWithTags struct {
 	UUIDHypenated      string  `faker:"uuid_hyphenated"`
 	UUID               string  `faker:"uuid_digit"`
 	Skip               string  `faker:"-"`
+	PaymentMethod      string  `faker:"oneof: cc, paypal, check, money order"` // oneof will randomly pick one of the comma-separated values supplied in the tag
+	AccountID          int     `faker:"oneof: 15, 27, 61"`                     // use commas to separate the values for now. Future support for other separator characters may be added
 }
 
 func Example_withTags() {
@@ -104,6 +106,8 @@ func Example_withTags() {
 			AmountWithCurrency: XBB 49257.100000,
 			UUIDHypenated: 8f8e4463-9560-4a38-9b0c-ef24481e4e27,
 			UUID: 90ea6479fd0e4940af741f0a87596b73,
+			PaymentMethod: paypal,
+			AccountID: 61
 			Skip:
 		}
 	*/
diff --git a/faker_test.go b/faker_test.go
index 80e4103..bfcc8f2 100644
--- a/faker_test.go
+++ b/faker_test.go
@@ -153,7 +153,7 @@ func (s SomeStruct) String() string {
 	SFloat64:%v
 	SBool: %v
 	Struct: %v
-	Time: %v 
+	Time: %v
 	Stime: %v
 	Currency: %v
 	Amount: %v
@@ -162,7 +162,7 @@ func (s SomeStruct) String() string {
 	HyphenatedID: %v
 
 	MapStringString: %v
-	MapStringStruct: %v 
+	MapStringStruct: %v
 	MapStringStructPointer: %v
 	}`, s.Inta, s.Int8, s.Int16, s.Int32,
 		s.Int64, s.Float32, s.Float64, s.UInta,
@@ -911,11 +911,6 @@ type School struct {
 	Location string
 }
 
-type CustomTypeOverSlice []byte
-type CustomThatUsesSlice struct {
-	UUID CustomTypeOverSlice `faker:"custom-type-over-slice"`
-}
-
 func TestExtend(t *testing.T) {
 	// This test is to ensure that faker can be extended new providers
 
@@ -969,13 +964,15 @@ func TestExtend(t *testing.T) {
 		}
 	})
 
-	/**
-	 * Before updates, this test would fail
-	 */
+	type CustomTypeOverSlice []byte
+	type CustomThatUsesSlice struct {
+		UUID CustomTypeOverSlice `faker:"custom-type-over-slice"`
+	}
+
 	t.Run("test-with-custom-slice-type", func(t *testing.T) {
 		a := CustomThatUsesSlice{}
 		err := AddProvider("custom-type-over-slice", func(v reflect.Value) (interface{}, error) {
-			return []byte{0, 1, 2, 3, 4}, nil
+			return CustomTypeOverSlice{0, 1, 2, 3, 4}, nil
 		})
 
 		if err != nil {
@@ -993,6 +990,40 @@ func TestExtend(t *testing.T) {
 		}
 	})
 
+	type MyInt int
+	type Sample struct {
+		Value []MyInt `faker:"myint"`
+	}
+
+	t.Run("test with type alias for int", func(t *testing.T) {
+		a := Sample{}
+		sliceLen := 10
+		err := AddProvider("myint", func(v reflect.Value) (interface{}, error) {
+			s1 := rand.NewSource(time.Now().UnixNano())
+			r1 := rand.New(s1)
+			r := make([]MyInt, sliceLen)
+			for i := range r {
+				r[i] = MyInt(r1.Intn(100))
+			}
+			return r, nil
+		})
+
+		if err != nil {
+			t.Error("Expected Not Error, But Got: ", err)
+		}
+
+		err = FakeData(&a)
+
+		if err != nil {
+			t.Error("Expected Not Error, But Got: ", err)
+		}
+
+		if len(a.Value) != sliceLen {
+			t.Errorf("Expected a slice of length %v but got %v", sliceLen, len(a.Value))
+		}
+
+	})
+
 }
 
 func TestTagAlreadyExists(t *testing.T) {
@@ -1223,6 +1254,234 @@ func TestUniqueFailure(t *testing.T) {
 	}
 }
 
+func TestOneOfTag(t *testing.T) {
+
+	type CustomOneString struct {
+		PaymentType string `faker:"oneof: credit card, paypal"`
+	}
+
+	t.Run("creates one of the desired string values", func(t *testing.T) {
+		a := CustomOneString{}
+		err := FakeData(&a)
+		if err != nil {
+			t.Errorf("expected no error, but got %v", err)
+		}
+		one := a.PaymentType == "credit card"
+		two := a.PaymentType == "paypal"
+
+		if !one && !two {
+			t.Errorf(
+				"expected either %v or %v but got %v",
+				"credit card",
+				"paypal",
+				a.PaymentType,
+			)
+		}
+	})
+
+	type CustomMultiString struct {
+		PaymentType string `faker:"oneof: cc, check, paypal, bank account"`
+	}
+	t.Run("creates only one of the desired string values from many", func(t *testing.T) {
+		a := CustomMultiString{}
+		err := FakeData(&a)
+		if err != nil {
+			t.Errorf("expected no error, but got %v", err)
+		}
+		one := a.PaymentType == "cc"
+		two := a.PaymentType == "paypal"
+		three := a.PaymentType == "check"
+		four := a.PaymentType == "bank account"
+
+		if !one && !two && !three && !four {
+			t.Errorf(
+				"expected either %v or %v or %v or %v but got %v",
+				"cc",
+				"paypal",
+				"check",
+				"bank account",
+				a.PaymentType,
+			)
+		}
+	})
+
+	type CustomOneofWrongString struct {
+		PaymentType string `faker:"oneof:"`
+	}
+
+	t.Run("errors when tag is not used correctly string no args", func(t *testing.T) {
+		a := CustomOneofWrongString{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Errorf("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrNotEnoughTagArguments
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomOneofWrongString2 struct {
+		PaymentType string `faker:"oneof: cc: check, bank"`
+	}
+
+	t.Run("errors when tag is not used correctly string invalid argument separator", func(t *testing.T) {
+		a := CustomOneofWrongString2{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Errorf("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrUnsupportedTagArguments
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomWrongString3 struct {
+		PaymentType string `faker:"oneof: credit card"`
+	}
+
+	t.Run("errors when tag is not used correctly string only one argument", func(t *testing.T) {
+		a := CustomWrongString3{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Errorf("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrNotEnoughTagArguments
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomWrongString4 struct {
+		PaymentType string `faker:"oneof: ,,,cc, credit card,,"`
+	}
+
+	t.Run("errors when tag is not used correctly string duplicate separator", func(t *testing.T) {
+		a := CustomWrongString4{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Errorf("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrDuplicateSeparator
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomOneofInt1 struct {
+		Age int `faker:"oneof: 16, 18, 21"`
+	}
+
+	t.Run("should pick one of the number args", func(t *testing.T) {
+		a := CustomOneofInt1{}
+		err := FakeData(&a)
+		if err != nil {
+			t.Errorf("expected no error, but got %v", err)
+		}
+		one := a.Age == 16
+		two := a.Age == 18
+		three := a.Age == 21
+		actual := a.Age
+		if !one && !two && !three {
+			t.Errorf(
+				"expected either %v, %v, or %v, but got %v",
+				16, 18, 21, actual,
+			)
+		}
+	})
+
+	type CustomOneofWrongInt struct {
+		Age int `faker:"oneof:"`
+	}
+
+	t.Run("errors when tag is not used correctly no args int", func(t *testing.T) {
+		a := CustomOneofWrongInt{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Errorf("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrNotEnoughTagArguments
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomOneofWrongInt2 struct {
+		Age int `faker:"oneof: 15: 18, 35"`
+	}
+
+	t.Run("errors when tag is not used correctly int invalid argument separator", func(t *testing.T) {
+		a := CustomOneofWrongInt2{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Errorf("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrUnsupportedTagArguments
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomOneofWrongInt3 struct {
+		Age int `faker:"oneof: 15, 18, oops"`
+	}
+
+	t.Run("errors when tag is not used correctly int invalid argument type", func(t *testing.T) {
+		a := CustomOneofWrongInt3{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Fatal("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrUnsupportedTagArguments
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomWrongInt4 struct {
+		Age int `faker:"oneof: 15"`
+	}
+
+	t.Run("errors when tag is not used correctly int only one argument", func(t *testing.T) {
+		a := CustomWrongInt4{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Fatal("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrNotEnoughTagArguments
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+	type CustomWrongInt5 struct {
+		Age int `faker:"oneof: 15,,16,17"`
+	}
+
+	t.Run("errors when tag is not used correctly int only one argument", func(t *testing.T) {
+		a := CustomWrongInt5{}
+		err := FakeData(&a)
+		if err == nil {
+			t.Fatal("expected error, but got no error")
+		}
+		actual := err.Error()
+		expected := ErrDuplicateSeparator
+		if actual != expected {
+			t.Errorf("expected %v, but got %v", expected, actual)
+		}
+	})
+
+}
+
 // getStringLen for language independent string length
 func utfLen(value string) int {
 	var r int