Skip to content

Commit dc4b70f

Browse files
committed
Improve tag parsing (regex support added)
1 parent d7073e7 commit dc4b70f

File tree

9 files changed

+175
-55
lines changed

9 files changed

+175
-55
lines changed

README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ The library provides the following features:
1616

1717
* Struct fields representation with ENUM.
1818
* Different functions and methods to work with ENUM (validation, listing, conversion to string, etc).
19-
* Tag-based field names extraction.
19+
* Tag-based field names extraction (regex can be used to extract a value from a tag).
2020
* Embedded fields extraction.
2121
* Fields excluding.
2222
* Different formatting (camel, pascal, snake).
2323
* Template overriding.
2424

2525
## Installation
2626

27-
$ go install github.com/kpeu3i/fielder@v1.4.0
27+
go install github.com/kpeu3i/fielder@v1.5.0
2828

2929
## Usage
3030

@@ -48,7 +48,7 @@ Then, run command bellow to generate the code:
4848

4949
$ go generate ./...
5050

51-
The following formatting strategies can be applied for the extracted field names (see `format` flag):
51+
The following formatting strategies can be applied for the extracted field names (see `format` and `tag_format` flag):
5252
* `snake_case` (e.g `first_name`)
5353
* `camel_case` (e.g `firstName`)
5454
* `pascal_case` (e.g `FirstName`)
@@ -60,23 +60,29 @@ The following CLI flags are allowed:
6060
```
6161
Usage of fielder:
6262
-embedded
63-
Extract embedded fields (default false)
63+
Extract embedded fields (default false)
6464
-excluded string
65-
Comma separated list of excluded fields (default "")
65+
Comma separated list of excluded fields (default "")
6666
-format string
67-
Format of the generated type values (default "as_is")
67+
Format of the generated type values extracted from the struct field (default "as_is")
6868
-output string
69-
Set output filename (default "<src_dir>/<type>_fielder.go")
69+
Set output filename (default "<src_dir>/<type>_fielder.go")
7070
-pkg string
71-
Package name to extract type from (default ".")
71+
Package name to extract type from (default ".")
7272
-suffix string
73-
Suffix for the generated struct (default "Field")
73+
Suffix for the generated struct (default "Field")
7474
-tag string
75-
Use tag to extract field values from (default "")
75+
Tag to extract field values from (default "")
76+
-tag_format string
77+
Format of the generated type values extracted from the tag (default "as_is")
78+
-tag_regexp string
79+
Regular expression to parse field value from a tag (default "")
80+
-tag_strict
81+
Strict mode for tag parsing (returns error if tag is not found)
7682
-tpl string
77-
Set template filename (default "")
83+
Set template filename (default "")
7884
-type string
79-
Type to extract fields from
85+
Type to extract fields from
8086
```
8187

8288
## Generated types, functions and methods

config.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ type config struct {
2121
Pkg string
2222
Type string
2323
Suffix string
24-
Tag string
2524
Format string
25+
Tag string
26+
TagRegex string
27+
TagFormat string
28+
TagStrict bool
2629
Embedded bool
2730
Excluded []string
2831
TemplateFilename string
@@ -33,8 +36,11 @@ func initConfig() (config, error) {
3336
pkg := flag.String("pkg", ".", "Package name to extract type from")
3437
typ := flag.String("type", "", "Type to extract fields from")
3538
suffix := flag.String("suffix", "Field", "Suffix for the generated struct")
36-
tag := flag.String("tag", "", `Use tag to extract field values from (default "")`)
37-
format := flag.String("format", formatAsIs, "Format of the generated type values")
39+
format := flag.String("format", formatAsIs, "Format of the generated type values extracted from the struct field")
40+
tag := flag.String("tag", "", `Tag to extract field values from (default "")`)
41+
tagRegex := flag.String("tag_regex", "", `Regular expression to parse field value from a tag (default "")`)
42+
tagFormat := flag.String("tag_format", formatAsIs, "Format of the generated type values extracted from the tag")
43+
tagStrict := flag.Bool("tag_strict", false, "Strict mode for tag parsing (returns error if tag is not found)")
3844
embedded := flag.Bool("embedded", false, "Extract embedded fields (default false)")
3945
excluded := flag.String("excluded", "", `Comma separated list of excluded fields (default "")`)
4046
templateFilename := flag.String("tpl", "", `Set template filename (default "")`)
@@ -46,18 +52,45 @@ func initConfig() (config, error) {
4652
return config{}, errors.New("type is required")
4753
}
4854

55+
if *tag == "" {
56+
if *tagRegex != "" {
57+
return config{}, errors.New("tag is required when tag_regex is set")
58+
}
59+
60+
if *tagFormat != formatAsIs {
61+
return config{}, errors.New("tag is required when tag_format is set")
62+
}
63+
64+
if *tagStrict != false {
65+
return config{}, errors.New("tag is required when tag_strict is set")
66+
}
67+
}
68+
69+
if *tagStrict && *format != formatAsIs {
70+
return config{}, errors.New("format is not supported when tag_strict is set")
71+
}
72+
4973
switch *format {
5074
case formatAsIs, formatSnakeCase, formatCamelCase, formatPascalCase:
5175
default:
5276
return config{}, fmt.Errorf("invalid format %s", *format)
5377
}
5478

79+
switch *tagFormat {
80+
case formatAsIs, formatSnakeCase, formatCamelCase, formatPascalCase:
81+
default:
82+
return config{}, fmt.Errorf("invalid tag_format %s", *format)
83+
}
84+
5585
conf := config{}
5686
conf.Pkg = *pkg
5787
conf.Type = *typ
5888
conf.Suffix = *suffix
59-
conf.Tag = *tag
6089
conf.Format = *format
90+
conf.Tag = *tag
91+
conf.TagRegex = *tagRegex
92+
conf.TagFormat = *tagFormat
93+
conf.TagStrict = *tagStrict
6194
conf.Embedded = *embedded
6295
conf.TemplateFilename = *templateFilename
6396

examples/simple/models/user_account.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:generate fielder -type=UserAccount -suffix=Column -embedded=true -tag=db -excluded=FullName
1+
//go:generate fielder -type=UserAccount -suffix=Column -embedded=true -tag=db -tag_strict=true -excluded=FullName
22

33
package models
44

examples/simple/models/user_account_fielder.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fileutil.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66
"os"
77
)
88

9-
func writeToFile(filename string, output []byte) error {
9+
func writeToFile(data []byte, filename string) error {
1010
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.ModePerm)
1111
if err != nil {
1212
return fmt.Errorf("could not open file %q: %v", filename, err)
1313
}
1414
defer f.Close()
1515

16-
_, err = f.Write(output)
16+
_, err = f.Write(data)
1717
if err != nil {
1818
return fmt.Errorf("could not write to file: %v", err)
1919
}

internal/testdata/user.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ type EntityDeletedAt struct {
1313
}
1414

1515
type Entity struct {
16-
ID string `db:"uuid"`
17-
CreatedAt time.Time `db:"created_at"`
18-
UpdatedAt time.Time `db:"updated_at"`
16+
ID string `db:"uuid"`
17+
CreatedAt time.Time `db:"created_at,type:timestamp"`
18+
UpdatedAt time.Time `db:"updated_at,type:timestamp"`
19+
FirstUpdatedAt time.Time
1920

2021
EntityID
2122
EntityDeletedAt

main.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ func main() {
1515
log.Fatalf("cannot parse type %s: %v", conf.Type, err)
1616
}
1717

18-
fields, err := parseFields(typ, conf.Tag, conf.Embedded, conf.Excluded, conf.Format, 0)
18+
parseParams := parseFieldsParams{
19+
format: conf.Format,
20+
tag: conf.Tag,
21+
tagRegex: conf.TagRegex,
22+
tagFormat: conf.TagFormat,
23+
tagStrict: conf.TagStrict,
24+
embedded: conf.Embedded,
25+
excluded: conf.Excluded,
26+
}
27+
28+
fields, err := parseFields(typ, parseParams, 0)
1929
if err != nil {
2030
log.Fatalf("cannot parse fields for type %s: %v", conf.Type, err)
2131
}
@@ -33,7 +43,7 @@ func main() {
3343
log.Fatalf("no output generated")
3444
}
3545

36-
err = writeToFile(conf.OutputFilename, output)
46+
err = writeToFile(output, conf.OutputFilename)
3747
if err != nil {
3848
log.Fatalf("cannot write to file %s: %v", conf.OutputFilename, err)
3949
}

parseutil.go

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"go/types"
7+
"regexp"
78

89
"github.com/fatih/structtag"
910
"github.com/iancoleman/strcase"
@@ -17,6 +18,16 @@ type field struct {
1718
Depth int
1819
}
1920

21+
type parseFieldsParams struct {
22+
format string
23+
tag string
24+
tagRegex string
25+
tagFormat string
26+
tagStrict bool
27+
embedded bool
28+
excluded []string
29+
}
30+
2031
func parseType(path, name string) (string, types.Type, error) {
2132
cfg := &packages.Config{
2233
Mode: packages.NeedTypes | packages.NeedImports | packages.NeedModule,
@@ -40,14 +51,7 @@ func parseType(path, name string) (string, types.Type, error) {
4051
return obj.Pkg().Name(), obj.Type(), nil
4152
}
4253

43-
func parseFields(
44-
typ types.Type,
45-
useTag string,
46-
useEmbedded bool,
47-
excluded []string,
48-
format string,
49-
depth int,
50-
) ([]field, error) {
54+
func parseFields(typ types.Type, params parseFieldsParams, depth int) ([]field, error) {
5155
strct, ok := typ.Underlying().(*types.Struct)
5256
if !ok {
5357
return nil, fmt.Errorf("type %s is not a struct", typ)
@@ -62,42 +66,70 @@ func parseFields(
6266
continue
6367
}
6468

65-
isExcluded := lo.Contains(excluded, strct.Field(i).Name())
69+
isExcluded := lo.Contains(params.excluded, strct.Field(i).Name())
6670
if isExcluded {
6771
continue
6872
}
6973

7074
if strct.Field(i).Embedded() {
71-
if !useEmbedded {
75+
if !params.embedded {
7276
continue
7377
}
7478

75-
embeddedFields, err := parseFields(strct.Field(i).Type(), useTag, useEmbedded, excluded, format, depth)
79+
embeddedFields, err := parseFields(strct.Field(i).Type(), params, depth)
7680
if err != nil {
7781
return nil, err
7882
}
7983

8084
fields = append(fields, embeddedFields...)
8185
} else {
8286
alias := strct.Field(i).Name()
83-
if useTag != "" {
87+
format := params.format
88+
89+
if params.tag != "" {
8490
tags, err := structtag.Parse(strct.Tag(i))
8591
if err != nil {
8692
return nil, err
8793
}
8894

89-
found := false
95+
var matchedTag *structtag.Tag
9096
for _, t := range tags.Tags() {
91-
if t.Key == useTag {
92-
alias = t.Name
93-
found = true
97+
if t.Key == params.tag {
98+
matchedTag = t
9499

95100
break
96101
}
97102
}
98103

99-
if !found {
100-
return nil, fmt.Errorf("tag %s not found for field %s", useTag, strct.Field(i).Name())
104+
if matchedTag != nil {
105+
if params.tagRegex == "" {
106+
alias = matchedTag.Name
107+
format = params.tagFormat
108+
} else {
109+
regex, err := regexp.Compile(params.tagRegex)
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
matches := regex.FindStringSubmatch(matchedTag.Value())
115+
if len(matches) < 2 {
116+
if params.tagStrict {
117+
return nil, fmt.Errorf(
118+
"tag %s of field %s does not match regex %s",
119+
matchedTag.Value(),
120+
strct.Field(i).Name(),
121+
params.tagRegex,
122+
)
123+
}
124+
} else {
125+
format = params.tagFormat
126+
alias = matches[1]
127+
}
128+
}
129+
} else {
130+
if params.tagStrict {
131+
return nil, fmt.Errorf("tag %s not found for field %s", params.tag, strct.Field(i).Name())
132+
}
101133
}
102134
}
103135

@@ -110,7 +142,7 @@ func parseFields(
110142
case formatPascalCase:
111143
alias = strcase.ToCamel(alias)
112144
default:
113-
return nil, fmt.Errorf("invalid format %s", format)
145+
return nil, fmt.Errorf("invalid format %s", params.format)
114146
}
115147

116148
fields = append(fields, field{

0 commit comments

Comments
 (0)