Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions example_template_tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package faker_test

import (
"fmt"

"github.com/go-faker/faker/v4"
)

type UserProfile struct {
FirstName string `faker:"first_name"`
LastName string `faker:"last_name"`
DomainName string `faker:"domain_name"`

// Template uses helper 'lower' and 'slug' (slug is lower+dash)
Username string `faker:"template:{{.FirstName | lower}}.{{.LastName | lower}}"`
Slug string `faker:"template:{{.FirstName | slug}}-{{.LastName | slug}}"`
Email string `faker:"template:{{.Username}}@{{.DomainName}}"`
// (only template-related fields kept for this example)
}

func Example_templateTag() {
var v UserProfile
if err := faker.FakeData(&v); err != nil {
fmt.Println("error:", err)
return
}
// output the filled struct; values are random so we don't assert exact output
fmt.Printf("%+v\n", v)
}
14 changes: 14 additions & 0 deletions faker.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
comma = ","
colon = ":"
ONEOF = "oneof"
TemplateTag = "template"
RussianFirstNameMaleTag = "russian_first_name_male"
RussianLastNameMaleTag = "russian_last_name_male"
RussianFirstNameFemaleTag = "russian_first_name_female"
Expand Down Expand Up @@ -395,7 +396,7 @@
return nil
}

func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, error) {

Check failure on line 399 in faker.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.23)

cyclomatic complexity 72 of func `getFakedValue` is high (> 70) (gocyclo)

Check failure on line 399 in faker.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.24)

cyclomatic complexity 72 of func `getFakedValue` is high (> 70) (gocyclo)

Check failure on line 399 in faker.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.21)

cyclomatic complexity 72 of func `getFakedValue` is high (> 70) (gocyclo)

Check failure on line 399 in faker.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.22)

cyclomatic complexity 72 of func `getFakedValue` is high (> 70) (gocyclo)
t := reflect.TypeOf(item)
if t == nil {
if opts.IgnoreInterface {
Expand Down Expand Up @@ -441,6 +442,8 @@
}
originalDataVal := reflect.ValueOf(item)
v := reflect.New(t).Elem()
// collect template fields to evaluate in a second pass
templateFields := make([]int, 0)
if opts.MaxFieldDepthOption == 0 {
return v, nil
} else if opts.MaxFieldDepthOption > 0 {
Expand All @@ -467,6 +470,11 @@
}

tags := decodeTags(t, i, opts.TagName)
// if this field is a template tag, defer evaluation until other fields are generated
if strings.HasPrefix(strings.ToLower(tags.fieldType), TemplateTag) {
templateFields = append(templateFields, i)
continue
}
switch {
case tags.keepOriginal:
zero, err := isZero(reflect.ValueOf(item).Field(i))
Expand Down Expand Up @@ -525,6 +533,12 @@
}

}
// second pass: evaluate template fields using values generated above
if len(templateFields) > 0 {
if err := EvaluateTemplateFields(t, v, templateFields, opts.TagName); err != nil {
return reflect.Value{}, err
}
}
return v, nil

case reflect.String:
Expand Down
89 changes: 89 additions & 0 deletions template_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package faker

import (
"bytes"
"fmt"
"reflect"
"strings"
"text/template"
)

// EvaluateTemplateFields evaluates the template-tagged fields for a struct value `v` of type `t`.
// tagName is the field tag name to use (typically opts.TagName).
func EvaluateTemplateFields(t reflect.Type, v reflect.Value, templateFields []int, tagName string) error {
if len(templateFields) == 0 {
return nil
}
// build context map from current struct values
// this will be updated as we compute each template field so
// later templates can reference earlier template results.
ctx := make(map[string]any)
for i := 0; i < v.NumField(); i++ {
ctx[t.Field(i).Name] = v.Field(i).Interface()
}

for _, idx := range templateFields {
tags := decodeTags(t, idx, tagName)
tpl := tags.fieldType
lower := strings.ToLower(tpl)
if strings.HasPrefix(lower, TemplateTag+":") {
tpl = tpl[len(TemplateTag)+1:]
}
res, err := evaluateTemplateForField(tpl, ctx)
if err != nil {
return err
}
if v.Field(idx).Kind() != reflect.String {
return fmt.Errorf("template tag only supported on string fields: %s", t.Field(idx).Name)
}

v.Field(idx).SetString(res)
// update context so subsequent template fields can reference it
ctx[t.Field(idx).Name] = res
}
return nil
}

// evaluateTemplateForField evaluates a template string tpl with context ctx (map of field name -> value)
// and returns the resulting string. Only a small set of helper funcs are exposed.
func evaluateTemplateForField(tpl string, ctx map[string]any) (string, error) {
t, err := template.New("faker_template").Funcs(helpers()).Parse(tpl)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := t.Execute(&buf, ctx); err != nil {
return "", err
}
return buf.String(), nil
}

func helpers() template.FuncMap {
return template.FuncMap{
"lower": strings.ToLower,
"upper": strings.ToUpper,
"slug": func(s string) string { return slugify(s) },
//can add more
}
}

// slugify is a tiny, permissive slug function: lowercases and replaces non-alphanumeric with '-'
func slugify(s string) string {
var b strings.Builder
s = strings.ToLower(s)
lastDash := false
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
lastDash = false
continue
}
if !lastDash {
b.WriteByte('-')
lastDash = true
}
}
res := b.String()
res = strings.Trim(res, "-")
return res
}
149 changes: 149 additions & 0 deletions template_tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package faker

import (
"reflect"
"testing"
)

func TestEvaluateTemplateFields(t *testing.T) {
cases := []struct {
name string
setup func() (reflect.Type, reflect.Value, []int)
expectErr bool
expectCheck func() error
}{
{
name: "email-lower",
setup: func() (reflect.Type, reflect.Value, []int) {
type T struct {
Name string
Email string `faker:"template:{{.Name | lower}}@example.com"`
}
var inst T
inst.Name = "John"

Check failure on line 23 in template_tag_test.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.23)

string `John` has 5 occurrences, make it a constant (goconst)

Check failure on line 23 in template_tag_test.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.24)

string `John` has 5 occurrences, make it a constant (goconst)

Check failure on line 23 in template_tag_test.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.21)

string `John` has 5 occurrences, make it a constant (goconst)

Check failure on line 23 in template_tag_test.go

View workflow job for this annotation

GitHub Actions / run-tests (~1.22)

string `John` has 5 occurrences, make it a constant (goconst)
return reflect.TypeOf(inst), reflect.ValueOf(&inst).Elem(), []int{1}
},
expectErr: false,
expectCheck: func() error {
return nil
},
},
{
name: "multiple",
setup: func() (reflect.Type, reflect.Value, []int) {
type T struct {
Name string
Email string `faker:"template:{{.Name | lower}}@example.com"`
Username string `faker:"template:{{.Name | lower}}"`
}
var inst T
inst.Name = "John"
// template fields are at indices 1 and 2
return reflect.TypeOf(inst), reflect.ValueOf(&inst).Elem(), []int{1, 2}
},
expectErr: false,
},
{
name: "chained",
setup: func() (reflect.Type, reflect.Value, []int) {
type T struct {
Name string
Slug string `faker:"template:{{.Name | slug}}"`
Email string `faker:"template:{{.Slug}}@example.com"`
}
var inst T
inst.Name = "John"
return reflect.TypeOf(inst), reflect.ValueOf(&inst).Elem(), []int{1, 2}
},
expectErr: false,
},
{
name: "slug",
setup: func() (reflect.Type, reflect.Value, []int) {
type T struct {
Name string
Slug string `faker:"template:{{.Name | slug}}"`
}
var inst T
inst.Name = "Hello, World!"
return reflect.TypeOf(inst), reflect.ValueOf(&inst).Elem(), []int{1}
},
expectErr: false,
},
{
name: "nested",
setup: func() (reflect.Type, reflect.Value, []int) {
type Addr struct{ City string }
type T struct {
Addr Addr
Desc string `faker:"template:City is {{.Addr.City}}"`
}
var inst T
inst.Addr.City = "Jakarta"
return reflect.TypeOf(inst), reflect.ValueOf(&inst).Elem(), []int{1}
},
expectErr: false,
},
{
name: "order",
setup: func() (reflect.Type, reflect.Value, []int) {
type T struct {
Email string `faker:"template:{{.Name | lower}}@example.com"`
Name string
}
var inst T
inst.Name = "John"
return reflect.TypeOf(inst), reflect.ValueOf(&inst).Elem(), []int{0}
},
expectErr: false,
},
{
name: "nonstring-error",
setup: func() (reflect.Type, reflect.Value, []int) {
type T struct {
Name string
Age int `faker:"template:{{.Name}}"`
}
var inst T
inst.Name = "John"
return reflect.TypeOf(inst), reflect.ValueOf(&inst).Elem(), []int{1}
},
expectErr: true,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
rt, rv, fields := c.setup()
err := EvaluateTemplateFields(rt, rv, fields, "faker")
if c.expectErr {
if err == nil {
t.Fatalf("expected error for case %s", c.name)
}
return
}
if err != nil {
t.Fatalf("unexpected error for case %s: %v", c.name, err)
}
// basic sanity checks per-case
switch c.name {
case "email-lower":
if rv.Field(1).String() != "[email protected]" {
t.Fatalf("email-lower mismatch: %q", rv.Field(1).String())
}
case "slug":
if rv.Field(1).String() != "hello-world" {
t.Fatalf("slug mismatch: %q", rv.Field(1).String())
}
case "nested":
if rv.Field(1).String() != "City is Jakarta" {
t.Fatalf("nested mismatch: %q", rv.Field(1).String())
}
case "order":
if rv.Field(0).String() != "[email protected]" {
t.Fatalf("order mismatch: %q", rv.Field(0).String())
}
}
})
}
}
Loading