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/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.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) 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