diff --git a/example_template_tag_test.go b/example_template_tag_test.go new file mode 100644 index 0000000..be008a7 --- /dev/null +++ b/example_template_tag_test.go @@ -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) +} diff --git a/faker.go b/faker.go index a4f66f4..4cd875e 100644 --- a/faker.go +++ b/faker.go @@ -90,6 +90,7 @@ const ( comma = "," colon = ":" ONEOF = "oneof" + TemplateTag = "template" RussianFirstNameMaleTag = "russian_first_name_male" RussianLastNameMaleTag = "russian_last_name_male" RussianFirstNameFemaleTag = "russian_first_name_female" @@ -441,6 +442,8 @@ func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, erro } 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 { @@ -467,6 +470,11 @@ func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, erro } 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)) @@ -525,6 +533,12 @@ func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, erro } } + // 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: diff --git a/template_tag.go b/template_tag.go new file mode 100644 index 0000000..8521e16 --- /dev/null +++ b/template_tag.go @@ -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 +} diff --git a/template_tag_test.go b/template_tag_test.go new file mode 100644 index 0000000..b0517eb --- /dev/null +++ b/template_tag_test.go @@ -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" + 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() != "john@example.com" { + 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() != "john@example.com" { + t.Fatalf("order mismatch: %q", rv.Field(0).String()) + } + } + }) + } +}