diff --git a/csv.go b/csv.go index d8294f41..29e01704 100644 --- a/csv.go +++ b/csv.go @@ -20,16 +20,16 @@ type CSVOptions struct { // CSV generates an object or an array of objects in json format // A nil CSVOptions returns a randomly structured CSV. -func CSV(co *CSVOptions) ([]byte, error) { return csvFunc(globalFaker.Rand, co) } +func CSV(co *CSVOptions) ([]byte, error) { return csvFunc(globalFaker, co) } // CSV generates an object or an array of objects in json format // A nil CSVOptions returns a randomly structured CSV. -func (f *Faker) CSV(co *CSVOptions) ([]byte, error) { return csvFunc(f.Rand, co) } +func (f *Faker) CSV(co *CSVOptions) ([]byte, error) { return csvFunc(f, co) } -func csvFunc(r *rand.Rand, co *CSVOptions) ([]byte, error) { +func csvFunc(f *Faker, co *CSVOptions) ([]byte, error) { if co == nil { // We didn't get a CSVOptions, so create a new random one - err := Struct(&co) + err := f.Struct(&co) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func csvFunc(r *rand.Rand, co *CSVOptions) ([]byte, error) { return nil, errors.New("invalid function, " + field.Function + " does not exist") } - value, err := funcInfo.Generate(r, &field.Params, funcInfo) + value, err := funcInfo.Generate(f.Rand, &field.Params, funcInfo) if err != nil { return nil, err } @@ -175,7 +175,8 @@ func addFileCSVLookup() { } co.Delimiter = delimiter - csvOut, err := csvFunc(r, &co) + f := &Faker{Rand: r} + csvOut, err := csvFunc(f, &co) if err != nil { return nil, err } diff --git a/json.go b/json.go index 46c8daac..dff59a94 100644 --- a/json.go +++ b/json.go @@ -4,7 +4,10 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "math/rand" + "reflect" + "strconv" ) // JSONOptions defines values needed for json generation @@ -56,17 +59,17 @@ func (okv jsonOrderedKeyVal) MarshalJSON() ([]byte, error) { // JSON generates an object or an array of objects in json format. // A nil JSONOptions returns a randomly structured JSON. -func JSON(jo *JSONOptions) ([]byte, error) { return jsonFunc(globalFaker.Rand, jo) } +func JSON(jo *JSONOptions) ([]byte, error) { return jsonFunc(globalFaker, jo) } // JSON generates an object or an array of objects in json format. // A nil JSONOptions returns a randomly structured JSON. -func (f *Faker) JSON(jo *JSONOptions) ([]byte, error) { return jsonFunc(f.Rand, jo) } +func (f *Faker) JSON(jo *JSONOptions) ([]byte, error) { return jsonFunc(f, jo) } // JSON generates an object or an array of objects in json format -func jsonFunc(r *rand.Rand, jo *JSONOptions) ([]byte, error) { +func jsonFunc(f *Faker, jo *JSONOptions) ([]byte, error) { if jo == nil { // We didn't get a JSONOptions, so create a new random one - err := Struct(&jo) + err := f.Struct(&jo) if err != nil { return nil, err } @@ -99,7 +102,7 @@ func jsonFunc(r *rand.Rand, jo *JSONOptions) ([]byte, error) { } // Call function value - value, err := funcInfo.Generate(r, &field.Params, funcInfo) + value, err := funcInfo.Generate(f.Rand, &field.Params, funcInfo) if err != nil { return nil, err } @@ -153,7 +156,7 @@ func jsonFunc(r *rand.Rand, jo *JSONOptions) ([]byte, error) { } // Call function value - value, err := funcInfo.Generate(r, &field.Params, funcInfo) + value, err := funcInfo.Generate(f.Rand, &field.Params, funcInfo) if err != nil { return nil, err } @@ -244,7 +247,89 @@ func addFileJSONLookup() { } jo.Indent = indent - return jsonFunc(r, &jo) + f := &Faker{Rand: r} + return jsonFunc(f, &jo) }, }) } + +// encoding/json.RawMessage is a special case of []byte +// it cannot be handled as a reflect.Array/reflect.Slice +// because it needs additional structure in the output +func rJsonRawMessage(f *Faker, t reflect.Type, v reflect.Value, tag string, size int) error { + b, err := f.JSON(nil) + if err != nil { + return err + } + + v.SetBytes(b) + return nil +} + +// encoding/json.Number is a special case of string +// that represents a JSON number literal. +// It cannot be handled as a string because it needs to +// represent an integer or a floating-point number. +func rJsonNumber(f *Faker, t reflect.Type, v reflect.Value, tag string, size int) error { + var ret json.Number + + var numberType string + + if tag == "" { + numberType = f.RandomString([]string{"int", "float"}) + + switch numberType { + case "int": + retInt := f.Int16() + ret = json.Number(strconv.Itoa(int(retInt))) + case "float": + retFloat := f.Float64() + ret = json.Number(strconv.FormatFloat(retFloat, 'f', -1, 64)) + } + } else { + fName, fParams := parseNameAndParamsFromTag(tag) + info := GetFuncLookup(fName) + if info == nil { + return fmt.Errorf("invalid function, %s does not exist", fName) + } + + // Parse map params + mapParams := parseMapParams(info, fParams) + + valueIface, err := info.Generate(f.Rand, mapParams, info) + if err != nil { + return err + } + + switch value := valueIface.(type) { + case int: + ret = json.Number(strconv.FormatInt(int64(value), 10)) + case int8: + ret = json.Number(strconv.FormatInt(int64(value), 10)) + case int16: + ret = json.Number(strconv.FormatInt(int64(value), 10)) + case int32: + ret = json.Number(strconv.FormatInt(int64(value), 10)) + case int64: + ret = json.Number(strconv.FormatInt(int64(value), 10)) + case uint: + ret = json.Number(strconv.FormatUint(uint64(value), 10)) + case uint8: + ret = json.Number(strconv.FormatUint(uint64(value), 10)) + case uint16: + ret = json.Number(strconv.FormatUint(uint64(value), 10)) + case uint32: + ret = json.Number(strconv.FormatUint(uint64(value), 10)) + case uint64: + ret = json.Number(strconv.FormatUint(uint64(value), 10)) + case float32: + ret = json.Number(strconv.FormatFloat(float64(value), 'f', -1, 64)) + case float64: + ret = json.Number(strconv.FormatFloat(float64(value), 'f', -1, 64)) + default: + return fmt.Errorf("invalid type, %s is not a valid type for json.Number", reflect.TypeOf(value)) + } + } + v.Set(reflect.ValueOf(ret)) + return nil +} diff --git a/json_test.go b/json_test.go index c5b6752d..c8b6a848 100644 --- a/json_test.go +++ b/json_test.go @@ -362,6 +362,124 @@ func TestJSONNoOptions(t *testing.T) { } } +func TestJSONRawMessage(t *testing.T) { + type J struct { + Field json.RawMessage `json:"field"` + } + + Seed(100) + + var objs []J + Slice(&objs) + + _, err := json.Marshal(objs) + if err != nil { + t.Fatal(err) + } +} + +func TestJSONRawMessageWithTag(t *testing.T) { + type J struct { + Field json.RawMessage `json:"field" faker:"json"` + } + + Seed(100) + + var objs []J + Slice(&objs) + + _, err := json.Marshal(objs) + if err != nil { + t.Fatal(err) + } +} + +func TestJSONNumber(t *testing.T) { + type J struct { + Field json.Number `json:"field"` + } + + Seed(100) + + var objs []J + Slice(&objs) + + _, err := json.Marshal(objs) + if err != nil { + t.Fatal(err) + } +} + +func TestJSONNumberWithTag(t *testing.T) { + type J struct { + Field json.Number `json:"field" fake:"number:3,7"` + } + + Seed(100) + + var objs []J + Slice(&objs) + + got, err := objs[0].Field.Int64() + if err != nil { + t.Fatal(err) + } + if got < 3 || got > 7 { + t.Errorf("Expected a number between 3 and 7, got %d", got) + } + + _, err = json.Marshal(objs) + if err != nil { + t.Fatal(err) + } +} + +func ExampleJSONNumberWithTag() { + Seed(10) + + type J struct { + FieldNumber json.Number `fake:"number:3,7"` + FieldInt8 json.Number `fake:"int8"` + FieldInt16 json.Number `fake:"int16"` + FieldInt32 json.Number `fake:"int32"` + FieldInt64 json.Number `fake:"int64"` + FieldUint8 json.Number `fake:"uint8"` + FieldUint16 json.Number `fake:"uint16"` + FieldUint32 json.Number `fake:"uint32"` + FieldUint64 json.Number `fake:"uint64"` + FieldFloat32 json.Number `fake:"float32"` + FieldFloat64 json.Number `fake:"float64range:12,72"` + } + + var obj J + Struct(&obj) + + fmt.Printf("obj.FieldNumber = %+v\n", obj.FieldNumber) + fmt.Printf("obj.FieldInt8 = %+v\n", obj.FieldInt8) + fmt.Printf("obj.FieldInt16 = %+v\n", obj.FieldInt16) + fmt.Printf("obj.FieldInt32 = %+v\n", obj.FieldInt32) + fmt.Printf("obj.FieldInt64 = %+v\n", obj.FieldInt64) + fmt.Printf("obj.FieldUint8 = %+v\n", obj.FieldUint8) + fmt.Printf("obj.FieldUint16 = %+v\n", obj.FieldUint16) + fmt.Printf("obj.FieldUint32 = %+v\n", obj.FieldUint32) + fmt.Printf("obj.FieldUint64 = %+v\n", obj.FieldUint64) + fmt.Printf("obj.FieldFloat32 = %+v\n", obj.FieldFloat32) + fmt.Printf("obj.FieldFloat64 = %+v\n", obj.FieldFloat64) + + // Output: + // obj.FieldNumber = 3 + // obj.FieldInt8 = 16 + // obj.FieldInt16 = 10619 + // obj.FieldInt32 = -1654523813 + // obj.FieldInt64 = -4710905755560118665 + // obj.FieldUint8 = 200 + // obj.FieldUint16 = 28555 + // obj.FieldUint32 = 162876094 + // obj.FieldUint64 = 7956601014869229133 + // obj.FieldFloat32 = 9227009415507442000000000000000000000 + // obj.FieldFloat64 = 62.323882731848215 +} + func BenchmarkJSONLookup100(b *testing.B) { faker := New(0) diff --git a/lookup.go b/lookup.go index e5c4029f..5d4ded97 100644 --- a/lookup.go +++ b/lookup.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "reflect" + "sort" "strconv" "strings" "sync" @@ -17,11 +18,11 @@ var lockFuncLookups sync.Mutex // internalFuncLookups is the internal map array with mapping to all available data var internalFuncLookups map[string]Info = map[string]Info{ "internal_exampleFields": { - Description: "Example fields for generating xml and json", - Example: `{"name":"{firstname}","age":"{number:1,100}"}`, + Description: "Example fields for generating csv, json and xml", Output: "gofakeit.Field", Generate: func(r *rand.Rand, m *MapParams, info *Info) (interface{}, error) { - name, _ := getRandomFuncLookup(r, true) + name, _ := getRandomFuncLookup(r, excludeWithParams, + validTypes("string", "int", "[]string", "[]int")) return Field{ Name: name, Function: name, @@ -30,15 +31,40 @@ var internalFuncLookups map[string]Info = map[string]Info{ }, } -func getRandomFuncLookup(r *rand.Rand, excludeWithParams bool) (string, Info) { +// filterFuncLookup returns true when the lookup should be accepted +type filterFuncLookup func(Info) bool + +var ( + excludeWithParams filterFuncLookup = func(info Info) bool { + return len(info.Params) == 0 + } + + validTypes = func(acceptedTypes ...string) filterFuncLookup { + return func(info Info) bool { + for _, t := range acceptedTypes { + if info.Output == t { + return true + } + } + return false + } + } +) + +func getRandomFuncLookup(r *rand.Rand, filters ...filterFuncLookup) (string, Info) { var keys []string for k, v := range FuncLookups { - if excludeWithParams && len(v.Params) != 0 { - continue + isValid := true + for _, filter := range filters { + isValid = isValid && filter(v) + } + if isValid { + keys = append(keys, k) } - keys = append(keys, k) } + sort.Stable(sort.StringSlice(keys)) + selected := keys[r.Intn(len(keys))] return selected, FuncLookups[selected] } diff --git a/struct.go b/struct.go index f97ebecc..cc00c110 100644 --- a/struct.go +++ b/struct.go @@ -29,6 +29,24 @@ func structFunc(f *Faker, v interface{}) error { } func r(f *Faker, t reflect.Type, v reflect.Value, tag string, size int) error { + // Handle special types + + if t.PkgPath() == "encoding/json" { + // encoding/json has two special types: + // - RawMessage + // - Number + + switch t.Name() { + case "RawMessage": + return rJsonRawMessage(f, t, v, tag, size) + case "Number": + return rJsonNumber(f, t, v, tag, size) + default: + return errors.New("unknown encoding/json type: " + t.Name()) + } + } + + // Handle generic types switch t.Kind() { case reflect.Ptr: return rPointer(f, t, v, tag, size) diff --git a/xml.go b/xml.go index b34caa53..27b3ad03 100644 --- a/xml.go +++ b/xml.go @@ -129,16 +129,16 @@ func xmlMapLoop(e *xml.Encoder, m *xmlMap) error { // XML generates an object or an array of objects in json format // A nil XMLOptions returns a randomly structured XML. -func XML(xo *XMLOptions) ([]byte, error) { return xmlFunc(globalFaker.Rand, xo) } +func XML(xo *XMLOptions) ([]byte, error) { return xmlFunc(globalFaker, xo) } // XML generates an object or an array of objects in json format // A nil XMLOptions returns a randomly structured XML. -func (f *Faker) XML(xo *XMLOptions) ([]byte, error) { return xmlFunc(f.Rand, xo) } +func (f *Faker) XML(xo *XMLOptions) ([]byte, error) { return xmlFunc(f, xo) } -func xmlFunc(r *rand.Rand, xo *XMLOptions) ([]byte, error) { +func xmlFunc(f *Faker, xo *XMLOptions) ([]byte, error) { if xo == nil { // We didn't get a XMLOptions, so create a new random one - err := Struct(&xo) + err := f.Struct(&xo) if err != nil { return nil, err } @@ -185,7 +185,7 @@ func xmlFunc(r *rand.Rand, xo *XMLOptions) ([]byte, error) { return nil, errors.New("invalid function, " + field.Function + " does not exist") } - value, err := funcInfo.Generate(r, &field.Params, funcInfo) + value, err := funcInfo.Generate(f.Rand, &field.Params, funcInfo) if err != nil { return nil, err } @@ -238,7 +238,7 @@ func xmlFunc(r *rand.Rand, xo *XMLOptions) ([]byte, error) { return nil, errors.New("invalid function, " + field.Function + " does not exist") } - value, err := funcInfo.Generate(r, &field.Params, funcInfo) + value, err := funcInfo.Generate(f.Rand, &field.Params, funcInfo) if err != nil { return nil, err } @@ -346,7 +346,8 @@ func addFileXMLLookup() { } xo.Indent = indent - return xmlFunc(r, &xo) + f := &Faker{Rand: r} + return xmlFunc(f, &xo) }, }) }