niki/vendor/github.com/brianvoe/gofakeit/v7/generate.go

626 lines
16 KiB
Go

package gofakeit
import (
"encoding/json"
"errors"
"fmt"
"math"
"regexp/syntax"
"strings"
)
// Generate fake information from given string.
// Replaceable values should be within {}
//
// Functions
// Ex: {firstname} - billy
// Ex: {sentence:3} - Record river mind.
// Ex: {number:1,10} - 4
// Ex: {uuid} - 590c1440-9888-45b0-bd51-a817ee07c3f2
//
// Letters/Numbers
// Ex: ### - 481 - random numbers
// Ex: ??? - fda - random letters
//
// For a complete list of runnable functions use FuncsLookup
func Generate(dataVal string) (string, error) { return generate(GlobalFaker, dataVal) }
// Generate fake information from given string.
// Replaceable values should be within {}
//
// Functions
// Ex: {firstname} - billy
// Ex: {sentence:3} - Record river mind.
// Ex: {number:1,10} - 4
// Ex: {uuid} - 590c1440-9888-45b0-bd51-a817ee07c3f2
//
// Letters/Numbers
// Ex: ### - 481 - random numbers
// Ex: ??? - fda - random letters
//
// For a complete list of runnable functions use FuncsLookup
func (f *Faker) Generate(dataVal string) (string, error) { return generate(f, dataVal) }
func generate(f *Faker, dataVal string) (string, error) {
// Replace # with numbers and ? with letters
dataVal = replaceWithNumbers(f, dataVal)
dataVal = replaceWithLetters(f, dataVal)
// Check if string has any replaceable values
if !strings.Contains(dataVal, "{") {
return dataVal, nil
}
var result strings.Builder
result.Grow(len(dataVal) * 2) // Pre-allocate with estimate
i := 0
for i < len(dataVal) {
// Find next opening brace
start := strings.IndexByte(dataVal[i:], '{')
if start == -1 {
// No more replacements, append rest and break
result.WriteString(dataVal[i:])
break
}
start += i
// Append everything before the brace
result.WriteString(dataVal[i:start])
// Find matching closing brace (handle nested brackets)
end := -1
depth := 0
for j := start; j < len(dataVal); j++ {
if dataVal[j] == '{' {
depth++
} else if dataVal[j] == '}' {
depth--
if depth == 0 {
end = j
break
}
}
}
if end == -1 {
// No closing brace, append rest and break
result.WriteString(dataVal[start:])
break
}
// Extract function name and params
fParts := dataVal[start+1 : end]
fName, fParams, _ := strings.Cut(fParts, ":")
// Check if it's a replaceable lookup function
if info := GetFuncLookup(fName); info != nil {
// Get parameters
var mapParams *MapParams
paramsLen := len(info.Params)
if paramsLen > 0 && fParams != "" {
mapParams = NewMapParams()
// If just one param and its a string simply just pass it
if paramsLen == 1 && info.Params[0].Type == "string" {
mapParams.Add(info.Params[0].Field, fParams)
} else {
splitVals, err := funcLookupSplit(fParams)
if err != nil {
return "", err
}
mapParams, err = addSplitValsToMapParams(splitVals, info, mapParams)
if err != nil {
return "", err
}
}
if mapParams.Size() == 0 {
mapParams = nil
}
}
// Call function
fValue, err := info.Generate(f, mapParams, info)
if err != nil {
return "", err
}
// Write the generated value
result.WriteString(fmt.Sprintf("%v", fValue))
i = end + 1
} else {
// Not a valid function, keep the braces
result.WriteString(dataVal[start : end+1])
i = end + 1
}
}
return result.String(), nil
}
// FixedWidthOptions defines values needed for csv generation
type FixedWidthOptions struct {
RowCount int `json:"row_count" xml:"row_count" fake:"{number:1,10}"`
Fields []Field `json:"fields" xml:"fields" fake:"{fields}"`
}
// FixedWidth generates an table of random data in fixed width format
// A nil FixedWidthOptions returns a randomly structured FixedWidth.
func FixedWidth(co *FixedWidthOptions) (string, error) { return fixeWidthFunc(GlobalFaker, co) }
// FixedWidth generates an table of random data in fixed width format
// A nil FixedWidthOptions returns a randomly structured FixedWidth.
func (f *Faker) FixedWidth(co *FixedWidthOptions) (string, error) { return fixeWidthFunc(f, co) }
// Function to generate a fixed width document
func fixeWidthFunc(f *Faker, co *FixedWidthOptions) (string, error) {
// If we didn't get FixedWidthOptions, create a new random one
if co == nil {
co = &FixedWidthOptions{}
}
// Make sure you set a row count
if co.RowCount <= 0 {
co.RowCount = f.IntN(10) + 1
}
// Check fields
if len(co.Fields) <= 0 {
// Create random fields
co.Fields = []Field{
{Name: "Name", Function: "{firstname} {lastname}"},
{Name: "Email", Function: "email"},
{Name: "Password", Function: "password", Params: MapParams{"special": {"false"}, "space": {"false"}}},
}
}
data := [][]string{}
hasHeader := false
// Loop through fields, generate data and add to data array
for _, field := range co.Fields {
// Start new row
row := []string{}
// Add name to first value
if field.Name != "" {
hasHeader = true
}
row = append(row, field.Name)
// Get function
funcInfo := GetFuncLookup(field.Function)
var value any
if funcInfo == nil {
// Try to run the function through generate
for i := 0; i < co.RowCount; i++ {
genStr, err := generate(f, field.Function)
if err != nil {
return "", err
}
row = append(row, genStr)
}
} else {
// Generate function value
var err error
for i := 0; i < co.RowCount; i++ {
value, err = funcInfo.Generate(f, &field.Params, funcInfo)
if err != nil {
return "", err
}
// Add value to row
row = append(row, anyToString(value))
}
}
// Add row to data
data = append(data, row)
}
var result strings.Builder
// Calculate column widths
colWidths := make([]int, len(data))
for i, row := range data {
for _, value := range row {
width := len(value) + 5
if width > colWidths[i] {
colWidths[i] = width
}
}
}
// Append table rows to the string, excluding the entire row if the first value is empty
for i := 0; i < len(data[0]); i++ {
if !hasHeader && i == 0 {
continue // Skip the entire column if the first value is empty
}
var resultRow strings.Builder
for j, row := range data {
resultRow.WriteString(fmt.Sprintf("%-*s", colWidths[j], row[i]))
}
// Trim trailing spaces
result.WriteString(strings.TrimRight(resultRow.String(), " "))
// Only add new line if not the last row
if i != len(data[0])-1 {
result.WriteString("\n")
}
}
return result.String(), nil
}
// Regex will generate a string based upon a RE2 syntax
func Regex(regexStr string) string { return regex(GlobalFaker, regexStr) }
// Regex will generate a string based upon a RE2 syntax
func (f *Faker) Regex(regexStr string) string { return regex(f, regexStr) }
func regex(f *Faker, regexStr string) (gen string) {
re, err := syntax.Parse(regexStr, syntax.Perl)
if err != nil {
return "Could not parse regex string"
}
// Panic catch
defer func() {
if r := recover(); r != nil {
gen = fmt.Sprint(f)
return
}
}()
return regexGenerate(f, re, len(regexStr)*100)
}
func regexGenerate(f *Faker, re *syntax.Regexp, limit int) string {
if limit <= 0 {
panic("Length limit reached when generating output")
}
op := re.Op
switch op {
case syntax.OpNoMatch: // matches no strings
// Do Nothing
case syntax.OpEmptyMatch: // matches empty string
return ""
case syntax.OpLiteral: // matches Runes sequence
var b strings.Builder
for _, ru := range re.Rune {
b.WriteRune(ru)
}
return b.String()
case syntax.OpCharClass: // matches Runes interpreted as range pair list
// number of possible chars
sum := 0
for i := 0; i < len(re.Rune); i += 2 {
sum += int(re.Rune[i+1]-re.Rune[i]) + 1
if re.Rune[i+1] == 0x10ffff { // rune range end
sum = -1
break
}
}
// pick random char in range (inverse match group)
if sum == -1 {
chars := []uint8{}
for j := 0; j < len(allStr); j++ {
c := allStr[j]
// Check c in range
for i := 0; i < len(re.Rune); i += 2 {
if rune(c) >= re.Rune[i] && rune(c) <= re.Rune[i+1] {
chars = append(chars, c)
break
}
}
}
if len(chars) > 0 {
return string([]byte{chars[f.IntN(len(chars))]})
}
}
r := f.IntN(int(sum))
var ru rune
sum = 0
for i := 0; i < len(re.Rune); i += 2 {
gap := int(re.Rune[i+1]-re.Rune[i]) + 1
if sum+gap > r {
ru = re.Rune[i] + rune(r-sum)
break
}
sum += gap
}
return string(ru)
case syntax.OpAnyCharNotNL, syntax.OpAnyChar: // matches any character(and except newline)
return randCharacter(f, allStr)
case syntax.OpBeginLine: // matches empty string at beginning of line
case syntax.OpEndLine: // matches empty string at end of line
case syntax.OpBeginText: // matches empty string at beginning of text
case syntax.OpEndText: // matches empty string at end of text
case syntax.OpWordBoundary: // matches word boundary `\b`
case syntax.OpNoWordBoundary: // matches word non-boundary `\B`
case syntax.OpCapture: // capturing subexpression with index Cap, optional name Name
return regexGenerate(f, re.Sub0[0], limit)
case syntax.OpStar: // matches Sub[0] zero or more times
var b strings.Builder
for i := 0; i < number(f, 0, 10); i++ {
for _, rs := range re.Sub {
b.WriteString(regexGenerate(f, rs, limit-b.Len()))
}
}
return b.String()
case syntax.OpPlus: // matches Sub[0] one or more times
var b strings.Builder
for i := 0; i < number(f, 1, 10); i++ {
for _, rs := range re.Sub {
b.WriteString(regexGenerate(f, rs, limit-b.Len()))
}
}
return b.String()
case syntax.OpQuest: // matches Sub[0] zero or one times
var b strings.Builder
for i := 0; i < number(f, 0, 1); i++ {
for _, rs := range re.Sub {
b.WriteString(regexGenerate(f, rs, limit-b.Len()))
}
}
return b.String()
case syntax.OpRepeat: // matches Sub[0] at least Min times, at most Max (Max == -1 is no limit)
var b strings.Builder
count := 0
re.Max = int(math.Min(float64(re.Max), float64(10)))
if re.Max > re.Min {
count = f.IntN(re.Max - re.Min + 1)
}
for i := 0; i < re.Min || i < (re.Min+count); i++ {
for _, rs := range re.Sub {
b.WriteString(regexGenerate(f, rs, limit-b.Len()))
}
}
return b.String()
case syntax.OpConcat: // matches concatenation of Subs
var b strings.Builder
for _, rs := range re.Sub {
b.WriteString(regexGenerate(f, rs, limit-b.Len()))
}
return b.String()
case syntax.OpAlternate: // matches alternation of Subs
return regexGenerate(f, re.Sub[number(f, 0, len(re.Sub)-1)], limit)
}
return ""
}
// Map will generate a random set of map data
func Map() map[string]any { return mapFunc(GlobalFaker) }
// Map will generate a random set of map data
func (f *Faker) Map() map[string]any { return mapFunc(f) }
func mapFunc(f *Faker) map[string]any {
m := map[string]any{}
randWordType := func() string {
s := randomString(f, []string{"lorem", "bs", "job", "name", "address"})
switch s {
case "bs":
return bs(f)
case "job":
return jobTitle(f)
case "name":
return name(f)
case "address":
return street(f) + ", " + city(f) + ", " + state(f) + " " + zip(f)
}
return word(f)
}
randSlice := func() []string {
var sl []string
for ii := 0; ii < number(f, 3, 10); ii++ {
sl = append(sl, word(f))
}
return sl
}
for i := 0; i < number(f, 3, 10); i++ {
t := randomString(f, []string{"string", "int", "float", "slice", "map"})
switch t {
case "string":
m[word(f)] = randWordType()
case "int":
m[word(f)] = number(f, 1, 10000000)
case "float":
m[word(f)] = float32Range(f, 1, 1000000)
case "slice":
m[word(f)] = randSlice()
case "map":
mm := map[string]any{}
tt := randomString(f, []string{"string", "int", "float", "slice"})
switch tt {
case "string":
mm[word(f)] = randWordType()
case "int":
mm[word(f)] = number(f, 1, 10000000)
case "float":
mm[word(f)] = float32Range(f, 1, 1000000)
case "slice":
mm[word(f)] = randSlice()
}
m[word(f)] = mm
}
}
return m
}
func addGenerateLookup() {
AddFuncLookup("generate", Info{
Display: "Generate",
Category: "generate",
Description: "Random string generated from string value based upon available data sets",
Example: "{firstname} {lastname} {email} - Markus Moen markusmoen@pagac.net",
Output: "string",
Aliases: []string{
"template expander",
"placeholder interpolator",
"variable substitution",
"token formatter",
"pattern builder",
"macro resolver",
},
Keywords: []string{
"upon", "datasets", "random",
"string", "value", "available", "data",
"sets", "based",
},
Params: []Param{
{Field: "str", Display: "String", Type: "string", Description: "String value to generate from"},
},
Generate: func(f *Faker, m *MapParams, info *Info) (any, error) {
str, err := info.GetString(m, "str")
if err != nil {
return nil, err
}
// Limit the length of the string passed
if len(str) > 1000 {
return nil, errors.New("string length is too large. limit to 1000 characters")
}
return generate(f, str)
},
})
AddFuncLookup("fixed_width", Info{
Display: "Fixed Width",
Category: "generate",
Description: "Fixed width rows of output data based on input fields",
Example: `Name Email Password Age
Markus Moen sylvanmraz@murphy.net 6VlvH6qqXc7g 13
Alayna Wuckert santinostanton@carroll.biz g7sLrS0gEwLO 46
Lura Lockman zacherykuhic@feil.name S8gV7Z64KlHG 12`,
Output: "[]byte",
ContentType: "text/plain",
Aliases: []string{
"fixed rows", "columnar data", "padded text", "aligned output", "structured fields",
},
Keywords: []string{
"tabular", "data", "format", "alignment", "columns", "rows", "layout", "monospace", "table", "presentation",
},
Params: []Param{
{Field: "rowcount", Display: "Row Count", Type: "int", Default: "10", Description: "Number of rows"},
{Field: "fields", Display: "Fields", Type: "[]Field", Description: "Fields name, function and params"},
},
Generate: func(f *Faker, m *MapParams, info *Info) (any, error) {
co := FixedWidthOptions{}
rowCount, err := info.GetInt(m, "rowcount")
if err != nil {
return nil, err
}
co.RowCount = rowCount
fields, _ := info.GetStringArray(m, "fields")
// Check to make sure fields has length
if len(fields) > 0 {
co.Fields = make([]Field, len(fields))
for i, f := range fields {
// Unmarshal fields string into fields array
err = json.Unmarshal([]byte(f), &co.Fields[i])
if err != nil {
return nil, err
}
}
} else {
return nil, errors.New("missing fields")
}
out, err := fixeWidthFunc(f, &co)
if err != nil {
return nil, err
}
return out, nil
},
})
AddFuncLookup("regex", Info{
Display: "Regex",
Category: "generate",
Description: "Pattern-matching tool used in text processing to search and manipulate strings",
Example: "[abcdef]{5} - affec",
Output: "string",
Aliases: []string{
"regular expression",
"string matcher",
"text parser",
"pattern engine",
"token analyzer",
"rule evaluator",
},
Keywords: []string{
"strings", "re2", "syntax",
"pattern-matching", "tool", "search",
"validation", "compile", "replace",
},
Params: []Param{
{Field: "str", Display: "String", Type: "string", Description: "Regex RE2 syntax string"},
},
Generate: func(f *Faker, m *MapParams, info *Info) (any, error) {
str, err := info.GetString(m, "str")
if err != nil {
return nil, err
}
// Limit the length of the string passed
if len(str) > 500 {
return nil, errors.New("string length is too large. limit to 500 characters")
}
return regex(f, str), nil
},
})
AddFuncLookup("map", Info{
Display: "Map",
Category: "generate",
Description: "Data structure that stores key-value pairs",
Example: `{
"software": 7518355,
"that": ["despite", "pack", "whereas", "recently", "there", "anyone", "time", "read"],
"use": 683598,
"whom": "innovate",
"yourselves": 1987784
}`,
Output: "map[string]any",
ContentType: "application/json",
Aliases: []string{
"associative array",
"lookup table",
"symbol table",
"keyed collection",
"map structure",
"object store",
},
Keywords: []string{
"stores", "key", "value",
"dictionary", "hash", "collection",
"pairs", "keys", "values", "structure",
},
Generate: func(f *Faker, m *MapParams, info *Info) (any, error) {
return mapFunc(f), nil
},
})
}