Skip to content

Commit

Permalink
Merge pull request #233 from mgilbir/safe-json-rawmessage
Browse files Browse the repository at this point in the history
Safe json.RawMessage and json.Number
  • Loading branch information
brianvoe committed Apr 10, 2023
2 parents bdec0d0 + 3eccca0 commit 57b7fad
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 27 deletions.
13 changes: 7 additions & 6 deletions csv.go
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
99 changes: 92 additions & 7 deletions json.go
Expand Up @@ -4,7 +4,10 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math/rand"
"reflect"
"strconv"
)

// JSONOptions defines values needed for json generation
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
118 changes: 118 additions & 0 deletions json_test.go
Expand Up @@ -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)

Expand Down
40 changes: 33 additions & 7 deletions lookup.go
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math/rand"
"reflect"
"sort"
"strconv"
"strings"
"sync"
Expand All @@ -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,
Expand All @@ -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]
}
Expand Down
18 changes: 18 additions & 0 deletions struct.go
Expand Up @@ -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)
Expand Down

0 comments on commit 57b7fad

Please sign in to comment.