Skip to content
This repository has been archived by the owner on Dec 8, 2022. It is now read-only.

Commit

Permalink
feat(faker): unique data generation (#77)
Browse files Browse the repository at this point in the history
* Add unique tag and lorem

* Update documentation

* Factor single fake data functions

* Fix typo

* Implement unique generation for all single fake data func
  • Loading branch information
System-Glitch authored and bxcodec committed Nov 15, 2019
1 parent e8bcb70 commit d1bb356
Show file tree
Hide file tree
Showing 16 changed files with 388 additions and 83 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Struct Data Fake Generator

Faker will generate you a fake data based on your Struct.
Faker will generate you a fake data based on your Struct.

[![Build Status](https://travis-ci.org/bxcodec/faker.svg?branch=master)](https://travis-ci.org/bxcodec/faker)
[![codecov](https://codecov.io/gh/bxcodec/faker/branch/master/graph/badge.svg)](https://codecov.io/gh/bxcodec/faker)
Expand Down
12 changes: 12 additions & 0 deletions SingleFakeData.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ faker.Name() // => Mrs. Casandra Kiehn
---

```go
faker.Phonenumber() // -> 201-886-0269
faker.TollFreePhoneNumber() // => (777) 831-964572
faker.E164PhoneNumber() // => +724891571063
```
Expand All @@ -97,3 +98,14 @@ faker.UUIDHyphenated() // => 8f8e4463-9560-4a38-9b0c-ef24481e4e27
faker.UUIDDigit() // => 90ea6479fd0e4940af741f0a87596b73
```

### Unique values

---

```go
faker.SetGenerateUniqueValues(true) // Enable unique data generation on single fake data functions
faker.Word()
//...
faker.SetGenerateUniqueValues(false) // Disable unique data generation on single fake data functions
faker.ResetUnique() // Forget all generated unique values
```
24 changes: 24 additions & 0 deletions WithStructTag.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ Supported tag:
**Skip :**
* \-

**Unique :**
* unique

```go

package main
Expand Down Expand Up @@ -272,3 +275,24 @@ Result:
MIint:map[7:7 5:7 8:8 9:5 6:5]
}
```
## Unique values

```go
// SomeStruct ...
type SomeStruct struct {
Word string `faker:"word,unique"`
}

func main() {

for i := 0 ; i < 5 ; i++ { // Generate 5 structs having a unique word
a := SomeStruct{}
err := faker.FakeData(&a)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v", a)
}
faker.ResetUnique() // Forget all generated unique values. Allows to start generating another unrelated dataset.
}
```
16 changes: 10 additions & 6 deletions address.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ type Addresser interface {
// Address struct
type Address struct{}

func (i Address) latitute() float32 {
func (i Address) latitude() float32 {
return (rand.Float32() * 180) - 90
}

// Latitude sets latitude of the address
func (i Address) Latitude(v reflect.Value) (interface{}, error) {
kind := v.Kind()
val := i.latitute()
val := i.latitude()
if kind == reflect.Float32 {
return val, nil
}
Expand All @@ -62,12 +62,16 @@ func (i Address) Longitude(v reflect.Value) (interface{}, error) {

// Longitude get fake longitude randomly
func Longitude() float64 {
address := Address{}
return float64(address.longitude())
return singleFakeData(LONGITUDE, func() interface{} {
address := Address{}
return float64(address.longitude())
}).(float64)
}

// Latitude get fake latitude randomly
func Latitude() float64 {
address := Address{}
return float64(address.latitute())
return singleFakeData(LATITUDE, func() interface{} {
address := Address{}
return float64(address.latitude())
}).(float64)
}
66 changes: 44 additions & 22 deletions datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,10 @@ func (d DateTime) UnixTime(v reflect.Value) (interface{}, error) {

// UnixTime get unix time randomly
func UnixTime() int64 {
datetime := DateTime{}
return datetime.unixtime()
return singleFakeData(UnixTimeTag, func() interface{} {
datetime := DateTime{}
return datetime.unixtime()
}).(int64)
}

func (d DateTime) date() string {
Expand All @@ -676,8 +678,10 @@ func (d DateTime) Date(v reflect.Value) (interface{}, error) {

// Date get fake date in string randomly
func Date() string {
datetime := DateTime{}
return datetime.date()
return singleFakeData(DATE, func() interface{} {
datetime := DateTime{}
return datetime.date()
}).(string)
}

func (d DateTime) time() string {
Expand All @@ -691,8 +695,10 @@ func (d DateTime) Time(v reflect.Value) (interface{}, error) {

// TimeString get time randomly in string format
func TimeString() string {
datetime := DateTime{}
return datetime.time()
return singleFakeData(TIME, func() interface{} {
datetime := DateTime{}
return datetime.time()
}).(string)
}

func (d DateTime) monthName() string {
Expand All @@ -706,8 +712,10 @@ func (d DateTime) MonthName(v reflect.Value) (interface{}, error) {

// MonthName get month name randomly in string format
func MonthName() string {
datetime := DateTime{}
return datetime.monthName()
return singleFakeData(MonthNameTag, func() interface{} {
datetime := DateTime{}
return datetime.monthName()
}).(string)
}

func (d DateTime) year() string {
Expand All @@ -721,8 +729,10 @@ func (d DateTime) Year(v reflect.Value) (interface{}, error) {

// YearString get year randomly in string format
func YearString() string {
datetime := DateTime{}
return datetime.year()
return singleFakeData(YEAR, func() interface{} {
datetime := DateTime{}
return datetime.year()
}).(string)
}

func (d DateTime) dayOfWeek() string {
Expand All @@ -736,8 +746,10 @@ func (d DateTime) DayOfWeek(v reflect.Value) (interface{}, error) {

// DayOfWeek get day of week randomly in string format
func DayOfWeek() string {
datetime := DateTime{}
return datetime.dayOfWeek()
return singleFakeData(DayOfWeekTag, func() interface{} {
datetime := DateTime{}
return datetime.dayOfWeek()
}).(string)
}

func (d DateTime) dayOfMonth() string {
Expand All @@ -751,8 +763,10 @@ func (d DateTime) DayOfMonth(v reflect.Value) (interface{}, error) {

// DayOfMonth get month randomly in string format
func DayOfMonth() string {
datetime := DateTime{}
return datetime.dayOfMonth()
return singleFakeData(DayOfMonthTag, func() interface{} {
datetime := DateTime{}
return datetime.dayOfMonth()
}).(string)
}

func (d DateTime) timestamp() string {
Expand All @@ -766,8 +780,10 @@ func (d DateTime) Timestamp(v reflect.Value) (interface{}, error) {

// Timestamp get timestamp randomly in string format: 2006-01-02 15:04:05
func Timestamp() string {
datetime := DateTime{}
return datetime.timestamp()
return singleFakeData(TIMESTAMP, func() interface{} {
datetime := DateTime{}
return datetime.timestamp()
}).(string)
}

func (d DateTime) century() string {
Expand All @@ -781,8 +797,10 @@ func (d DateTime) Century(v reflect.Value) (interface{}, error) {

// Century get century randomly in string
func Century() string {
datetime := DateTime{}
return datetime.century()
return singleFakeData(CENTURY, func() interface{} {
datetime := DateTime{}
return datetime.century()
}).(string)
}

func (d DateTime) timezone() string {
Expand All @@ -796,8 +814,10 @@ func (d DateTime) TimeZone(v reflect.Value) (interface{}, error) {

// Timezone get timezone randomly in string
func Timezone() string {
datetime := DateTime{}
return datetime.timezone()
return singleFakeData(TIMEZONE, func() interface{} {
datetime := DateTime{}
return datetime.timezone()
}).(string)
}

func (d DateTime) period() string {
Expand All @@ -811,8 +831,10 @@ func (d DateTime) TimePeriod(v reflect.Value) (interface{}, error) {

// Timeperiod get timeperiod randomly in string (AM/PM)
func Timeperiod() string {
datetime := DateTime{}
return datetime.period()
return singleFakeData(TimePeriodTag, func() interface{} {
datetime := DateTime{}
return datetime.period()
}).(string)
}

// RandomUnixTime is a helper function returning random Unix time
Expand Down
67 changes: 67 additions & 0 deletions faker.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"strings"
"sync"
"time"

"github.com/bxcodec/faker/v3/support/slice"
)

var (
Expand All @@ -25,6 +27,10 @@ var (
nBoundary = numberBoundary{start: 0, end: 100}
//Sets the random size for slices and maps.
randomSize = 100
// Sets the single fake data generator to generate unique values
generateUniqueValues = false
// Unique values are kept in memory so the generator retries if the value already exists
uniqueValues = map[string][]interface{}{}
)

type numberBoundary struct {
Expand All @@ -37,9 +43,11 @@ const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
maxRetry = 10000 // max number of retry for unique values
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
tagName = "faker"
keep = "keep"
unique = "unique"
ID = "uuid_digit"
HyphenatedID = "uuid_hyphenated"
EmailTag = "email"
Expand Down Expand Up @@ -198,6 +206,7 @@ var (
ErrMoreArguments = "Passed more arguments than is possible : (%d)"
ErrNotSupportedPointer = "Use sample:=new(%s)\n faker.FakeData(sample) instead"
ErrSmallerThanZero = "Size:%d is smaller than zero."
ErrUniqueFailure = "Failed to generate a unique value for field \"%s\""

ErrStartValueBiggerThanEnd = "Start value can not be bigger than end value."
ErrWrongFormattedTag = "Tag \"%s\" is not written properly"
Expand All @@ -209,6 +218,17 @@ func init() {
rand.Seed(time.Now().UnixNano())
}

// ResetUnique is used to forget generated unique values.
// Call this when you're done generating a dataset.
func ResetUnique() {
uniqueValues = map[string][]interface{}{}
}

// SetGenerateUniqueValues allows to set the single fake data generator functions to generate unique data.
func SetGenerateUniqueValues(unique bool) {
generateUniqueValues = unique
}

// SetNilIfLenIsZero allows to set nil for the slice and maps, if size is 0.
func SetNilIfLenIsZero(setNil bool) {
shouldSetNil = setNil
Expand Down Expand Up @@ -352,6 +372,7 @@ func getValue(a interface{}) (reflect.Value, error) {
default:
originalDataVal := reflect.ValueOf(a)
v := reflect.New(t).Elem()
retry := 0 // error if cannot generate unique value after maxRetry tries
for i := 0; i < v.NumField(); i++ {
if !v.Field(i).CanSet() {
continue // to avoid panic to set on unexported field in struct
Expand Down Expand Up @@ -390,6 +411,24 @@ func getValue(a interface{}) (reflect.Value, error) {
}
}

if tags.unique {

if retry >= maxRetry {
return reflect.Value{}, fmt.Errorf(ErrUniqueFailure, reflect.TypeOf(a).Field(i).Name)
}

value := v.Field(i).Interface()
if slice.ContainsValue(uniqueValues[tags.fieldType], value) { // Retry if unique value already found
i--
retry++
continue
}
retry = 0
uniqueValues[tags.fieldType] = append(uniqueValues[tags.fieldType], value)
} else {
retry = 0
}

}
return v, nil
}
Expand Down Expand Up @@ -489,23 +528,29 @@ func decodeTags(typ reflect.Type, i int) structTag {
tags := strings.Split(typ.Field(i).Tag.Get(tagName), ",")

keepOriginal := false
uni := false
res := make([]string, 0)
for _, tag := range tags {
if tag == keep {
keepOriginal = true
continue
} else if tag == unique {
uni = true
continue
}
res = append(res, tag)
}

return structTag{
fieldType: strings.Join(res, ","),
unique: uni,
keepOriginal: keepOriginal,
}
}

type structTag struct {
fieldType string
unique bool
keepOriginal bool
}

Expand Down Expand Up @@ -821,3 +866,25 @@ func RandomInt(parameters ...int) (p []int, err error) {
}
return p, err
}

func generateUnique(dataType string, fn func() interface{}) (interface{}, error) {
for i := 0; i < maxRetry; i++ {
value := fn()
if !slice.ContainsValue(uniqueValues[dataType], value) { // Retry if unique value already found
uniqueValues[dataType] = append(uniqueValues[dataType], value)
return value, nil
}
}
return reflect.Value{}, fmt.Errorf(ErrUniqueFailure, dataType)
}

func singleFakeData(dataType string, fn func() interface{}) interface{} {
if generateUniqueValues {
v, err := generateUnique(dataType, fn)
if err != nil {
panic(err)
}
return v
}
return fn()
}
Loading

0 comments on commit d1bb356

Please sign in to comment.