Skip to content

Commit

Permalink
add field acronym support
Browse files Browse the repository at this point in the history
  • Loading branch information
mobiletoly committed Dec 8, 2021
1 parent 2773eb8 commit 8544d75
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 86 deletions.
107 changes: 54 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# GO Better - code generator for struct required fields

This project is an attempt to address lack of required fields in Go's struct types and to create a constructor that
will actually enforce specifying mandatory fields in constructor with the approach similar to "named arguments".
Named arguments will allow you to specify multiple arguments in constructor without concerns that you have to be
very careful passing arguments in correct order.
This project is an attempt to address lack of required fields in Go's struct types and to create a constructor that will
actually enforce specifying mandatory fields in constructor with the approach similar to "named arguments". Named
arguments will allow you to specify multiple arguments in constructor without concerns that you have to be very careful
passing arguments in correct order.

As you are aware, when you create a structure in Go - you cannot specify required fields. For example, if we have
a structure for Person such as
As you are aware, when you create a structure in Go - you cannot specify required fields. For example, if we have a
structure for Person such as

```
type Person struct {
Expand All @@ -27,10 +27,10 @@ var person = Person{
}
```

This is all good unless you have a different places where you have to create a `Person` structure, then when you
add new fields, it will become a challenge to scan through code to find all occurrences of creating Person. One of
the suggestions you can find is to create a constructor function with arguments representing required fields.
In this case you can create and fill `Person` structure with `NewPerson()` function. Here is an example:
This is all good unless you have a different places where you have to create a `Person` structure, then when you add new
fields, it will become a challenge to scan through code to find all occurrences of creating Person. One of the
suggestions you can find is to create a constructor function with arguments representing required fields. In this case
you can create and fill `Person` structure with `NewPerson()` function. Here is an example:

```
func NewPerson(firstName string, lastName string, age int) Person {
Expand All @@ -42,22 +42,22 @@ func NewPerson(firstName string, lastName string, age int) Person {
}
```

The typical call will be `person := NewPerson("Joe", "Doe", 40)`.
This is actually not a bad solution, but unfortunately it means that you have to manually update your `NewPerson`
function every time when you add or remove fields. Moreover, because Go does not have named parameters, you
need to be very careful when you move fields within the structure or add a new one, because you might start
passing wrong values. E.g. if you swap FirstName and LastName in Person structure then suddenly your call to `NewPerson`
The typical call will be `person := NewPerson("Joe", "Doe", 40)`. This is actually not a bad solution, but unfortunately
it means that you have to manually update your `NewPerson`
function every time when you add or remove fields. Moreover, because Go does not have named parameters, you need to be
very careful when you move fields within the structure or add a new one, because you might start passing wrong values.
E.g. if you swap FirstName and LastName in Person structure then suddenly your call to `NewPerson`
will be resulting in FirstName being "Doe" and LastName being "Joe". Compiler does not help us here.

**gobetter** addresses this issue by creating constructor function for you and by wrapping each parameter in its own
type. In this case compiler will raise an error if you missed a parameter or put it in a different order.
type. In this case compiler will raise an error if you missed a parameter or put it in a different order.

### Pre-requisites

You have to install two tools:

First one is **goimports** (if you don't have it already installed). It will be used by **gobetter** to optimize
imports and perform proper formatting.
First one is **goimports** (if you don't have it already installed). It will be used by **gobetter** to optimize imports
and perform proper formatting.

```shell
go get -u golang.org/x/tools/cmd/goimports
Expand All @@ -72,8 +72,8 @@ go get -u github.com/mobiletoly/gobetter
### Usage

Tool uses generation approach to create constructors with named arguments. First you have to add `go:generate` comment
into a file with a structures you want to create required parameters for, after that you can mark required fields
with a special comment. E.g. this is how your data structure to serialize/deserialize JSON is going to look like:
into a file with a structures you want to create required parameters for, after that you can mark required fields with a
special comment. E.g. this is how your data structure to serialize/deserialize JSON is going to look like:

```
//go:generate gobetter -input $GOFILE
Expand All @@ -82,6 +82,7 @@ package main
type Person struct { //+gob:Constructor
firstName string //+gob:getter
lastName string //+gob:getter
dob string //gob:getter +gob:acronym
Age int
Description string //+gob:_
}
Expand All @@ -90,21 +91,24 @@ type Person struct { //+gob:Constructor
(you can add field tags e.g. `json:"age"` into your struct if you need)

- `+gob:Constructor` comment serves as a flag and must be on the same line as struct (you can add more text to this
comment but flag needs to be a separate word). It instructs gobetter to generate argument structures and
constructor for this structure. Please read below to find out why "Constructor" starts with upper-cased "C".
comment but flag needs to be a separate word). It instructs gobetter to generate argument structures and constructor
for this structure. Please read below to find out why "Constructor" starts with upper-cased "C".


- `//+gob:getter` is to generate a getter for field, should be applied only for fields that start in lowercase (fields
that are not accessible outside of a package). It will effectively make these fields read-only for callers outside
a package.
that are not accessible outside of a package). It will effectively make these fields read-only for callers outside a
package.


- `//+gob:_` flag in comment hints gobetter that structure field is option and should not be added to
constructor.
- `//+gob:_` flag in comment hints gobetter that structure field is option and should not be added to constructor.


All you have to do now is to run `go generate` tool to generate go files that will be containing argument structures
as well as constructors for your structures.
- `//+gob:acronym` specifies that field is acronym. In our case `dob` (date of birth) will remain private field but
since it has getter - then getter will be generated as `DOB()` function (instead of `Dob()`).
Named parameters will be named using all upper-cased characters as well.

All you have to do now is to run `go generate` tool to generate go files that will be containing argument structures as
well as constructors for your structures.

```shell
go generate ./...
Expand All @@ -130,30 +134,28 @@ person.Description = "some description"
Unless you specify otherwise with comnand-line flags - gobetter only processes structures marked with `//+gob:`
comment annotations, and you have few options to choose from:

- `//+gob:Constructor` - generate upper-cased exported constructor in form of **NewClassName**. This flag is
honored only if class itself is exported (started with uppercase character), otherwise package-level lower-cased
- `//+gob:Constructor` - generate upper-cased exported constructor in form of **NewClassName**. This flag is honored
only if class itself is exported (started with uppercase character), otherwise package-level lower-cased
constructor **newClassName** will be generated;


- `//+gob:constructor` - generate package-level constructor in form of **newClassName** even for exported classes;


- `//+gob:_` - no constructor is generated. This flag is useful if you don't want to generate
constructor but still want for gobetter to process another fields, such as marked with `gob:getter` to generate
getters;

- `//+gob:ptr` - specifies that generated getter functions (if any) should be operated on pointer receivers
instead of value receiver. It means that `func (v *Person) FirstName() string` function will be generated instead
of default `func (v Person) FirstName() string`
- `//+gob:_` - no constructor is generated. This flag is useful if you don't want to generate constructor but still want
for gobetter to process another fields, such as marked with `gob:getter` to generate getters;

- `//+gob:ptr` - specifies that generated getter functions (if any) should be operated on pointer receivers instead of
value receiver. It means that `func (v *Person) FirstName() string` function will be generated instead of
default `func (v Person) FirstName() string`

### Integration with IntelliJ

It can be annoying to run `go generate ./...` from a terminal every time. Moreover, call this command will be generating
required fields support for all your files every time, while most of the time you want to do it on per-file basis.
The easiest approach for IntelliJ is to set up a FileWatcher for .go files and run generate command every time you
change a file. Depending on your OS - instructions can be slightly different but in overall they remain the same.
For Mac OS in your IntelliJ select from main menu **IntelliJ IDEA / Preferences / Tools / File Watcher** and add
required fields support for all your files every time, while most of the time you want to do it on per-file basis. The
easiest approach for IntelliJ is to set up a FileWatcher for .go files and run generate command every time you change a
file. Depending on your OS - instructions can be slightly different but in overall they remain the same. For Mac OS in
your IntelliJ select from main menu **IntelliJ IDEA / Preferences / Tools / File Watcher** and add
<custom> task. Name it `Go Generate files` and setup **Files type**: `Go files`, **Program**: `go`,
**Arguments**: `generate`.<br>
At this point it should work, but File Watcher will be monitoring your entire project directory and not only your own
Expand All @@ -171,23 +173,22 @@ This will do it. Now when you save Go file - `go generate` will be automatically

`-input <input-file-name>` - input file name where to read structures from

`-output <output-file-name>` - optional file name to save generated data into. if this switch is not specified
then gobetter will create a filename with suffix `_gob.go` in the same directory where the input file resides.
`-output <output-file-name>` - optional file name to save generated data into. if this switch is not specified then
gobetter will create a filename with suffix `_gob.go` in the same directory where the input file resides.

`-generate-for all|exported|annotated` - sometimes you don't want to annotate structures with *//+gob:*
constructor annotation, or you don't have this option, because files with a structures could be
auto-generated for you by some other tool. In this case you can invoke gobetter from some other file
and pass **-generate-for** flag to specify that you want to process structures that don't have
annotation comments. **all** value will process all exported and package-level structs while
constructor annotation, or you don't have this option, because files with a structures could be auto-generated for you
by some other tool. In this case you can invoke gobetter from some other file and pass **-generate-for** flag to specify
that you want to process structures that don't have annotation comments. **all** value will process all exported and
package-level structs while
**exported** will process only exported (started with uppercase character) structures. **annotated**
value disables automatic processing of structures (this is default behavior) and requires structure
annotation comments.
value disables automatic processing of structures (this is default behavior) and requires structure annotation comments.

`-constructor exported|package|none` - this flag makes sense only for structures processed by
**-generate-for** flag. **exported** value enforces creation of exported struct constructors (for
package-level structures package-level constructors will be generated). **package** value enforces
creation of package-level constructors for all structures. **none** means no constructorы will be
provided (but gobetter will process structure in order to generate getters if necessary).
**-generate-for** flag. **exported** value enforces creation of exported struct constructors (for package-level
structures package-level constructors will be generated). **package** value enforces creation of package-level
constructors for all structures. **none** means no constructorы will be provided (but gobetter will process structure in
order to generate getters if necessary).

Example:

Expand Down
5 changes: 2 additions & 3 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ type Person struct { //+gob:constructor
test strings.Builder //+gob:getter
test2 *ast.Scope //+gob:getter
test3 *map[string]interface{}
xml *string //+gob:GETTER

anotherPerson *anotherPerson
xml *string //+gob:getter +gob:acronym
anotherPerson *anotherPerson
}

type fixedPerson struct {
Expand Down
57 changes: 40 additions & 17 deletions gobld.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type StructParser struct {
flagReceiverPtrRegexp *regexp.Regexp
flagOptionalRegexp *regexp.Regexp
flagGetterRegexp *regexp.Regexp
flagUppercaseGetterRegexp *regexp.Regexp
flagAcronymRegex *regexp.Regexp
}

type GobBuilder struct {
Expand Down Expand Up @@ -59,30 +59,47 @@ func (bld *GobBuilder) appendImports() {
bld.common.WriteString(")\n\n")
}

func (bld *GobBuilder) appendArgStruct(structName string, fieldName string, fieldType string, structFlags StructFlags) (structArgName string) {
structArgName = newStructArgName(structName, fieldName, structFlags.Visibility)
func (bld *GobBuilder) appendArgStruct(
structName string, fieldName string, fieldType string, structFlags StructFlags, acronym bool,
) (structArgName string) {
var visibleFieldName string
if acronym {
visibleFieldName = strings.ToUpper(fieldName)
} else {
visibleFieldName = strings.Title(fieldName)
}
visibleStructName := convertStructNameAccordingToVisibility(structName, structFlags.Visibility)
structArgName = newStructArgName(structName, fieldName, structFlags.Visibility, acronym)
bld.common.WriteString(fmt.Sprintf("// %s represents field %s of struct %s\n", structArgName, fieldName, structName))
bld.common.WriteString(fmt.Sprintf("type %s struct {\n", structArgName))
bld.common.WriteString(fmt.Sprintf("\tArg %s\n}\n", fieldType))
bld.common.WriteString(fmt.Sprintf("// %s%s creates argument for field %s\n", structName, strings.Title(fieldName), fieldName))
bld.common.WriteString(fmt.Sprintf("// %s%s creates argument for field %s\n", visibleStructName, strings.Title(fieldName), fieldName))
bld.common.WriteString(fmt.Sprintf("func %s_%s(arg %s) %s {\n",
convertStructNameAccordingToVisibility(structName, structFlags.Visibility),
strings.Title(fieldName),
visibleStructName,
visibleFieldName,
fieldType, structArgName))
bld.common.WriteString(fmt.Sprintf("\treturn %s{Arg: arg}\n}\n\n", structArgName))
return
}

func (bld *GobBuilder) appendArgStructConstructor(structName string, fieldName string, fieldType string, visibility Visibility) (structArgName string) {
structArgName = newStructArgName(structName, fieldName, visibility)
func (bld *GobBuilder) appendArgStructConstructor(
structName string, fieldName string, fieldType string, visibility Visibility, acronym bool,
) (structArgName string) {
structArgName = newStructArgName(structName, fieldName, visibility, acronym)
bld.common.WriteString(fmt.Sprintf("func %s%s(arg %s) %s {\n", structName, fieldName,
fieldType, structArgName))
bld.common.WriteString(fmt.Sprintf("\treturn %s{Arg: arg}\n}\n\n", structArgName))
return
}

func newStructArgName(structName string, fieldName string, visibility Visibility) string {
return convertStructNameAccordingToVisibility(structName, visibility) + "_" + strings.Title(fieldName) + "_ArgWrapper"
func newStructArgName(structName string, fieldName string, visibility Visibility, acronym bool) string {
var title string
if acronym {
title = strings.ToUpper(fieldName)
} else {
title = strings.Title(fieldName)
}
return convertStructNameAccordingToVisibility(structName, visibility) + "_" + title + "_ArgWrapper"
}

func convertStructNameAccordingToVisibility(structName string, visibility Visibility) string {
Expand Down Expand Up @@ -115,8 +132,14 @@ func (bld *GobBuilder) appendBeginConstructorBody(structName string) {
bld.constructorPtrBody.WriteString(fmt.Sprintf("\treturn &%s{\n", structName))
}

func (bld *GobBuilder) appendConstructorArg(fieldName string, structArgName string) {
argName := "arg" + strings.Title(fieldName)
func (bld *GobBuilder) appendConstructorArg(fieldName string, structArgName string, acronym bool) {
var visibleFieldName string
if acronym {
visibleFieldName = strings.ToUpper(fieldName)
} else {
visibleFieldName = strings.Title(fieldName)
}
argName := "arg" + visibleFieldName
def := fmt.Sprintf("\t%s %s,\n", argName, structArgName)
value := fmt.Sprintf("\t\t%s: %s.Arg,\n", fieldName, argName)
bld.constructorValueDef.WriteString(def)
Expand All @@ -126,14 +149,14 @@ func (bld *GobBuilder) appendConstructorArg(fieldName string, structArgName stri
}

func (bld *GobBuilder) appendGetter(
structName string, fieldName string, fieldType string, flags StructFlags, allUppercase bool,
structName string, fieldName string, fieldType string, flags StructFlags, acronym bool,
) {
ptr := ""
if flags.PtrReceiver {
ptr = "*"
}
var addedFieldName string
if allUppercase {
if acronym {
addedFieldName = strings.ToUpper(fieldName)
} else {
addedFieldName = strings.Title(fieldName)
Expand Down Expand Up @@ -184,7 +207,7 @@ func NewStructParser(fileSet *token.FileSet, fileContent []byte) StructParser {
flagReceiverPtrRegexp: regexp.MustCompile(`\b+gob:ptr\b`),
flagOptionalRegexp: regexp.MustCompile(`\b+gob:_\b`),
flagGetterRegexp: regexp.MustCompile(`\b+gob:getter\b`),
flagUppercaseGetterRegexp: regexp.MustCompile(`\b+gob:GETTER\b`),
flagAcronymRegex: regexp.MustCompile(`\b+gob:acronym\b`),
}
}

Expand All @@ -202,8 +225,8 @@ func (sp *StructParser) fieldGetter(field *ast.Field) bool {
return sp.flagGetterRegexp.MatchString(field.Comment.Text())
}

func (sp *StructParser) fieldUppercaseGetter(field *ast.Field) bool {
return sp.flagUppercaseGetterRegexp.MatchString(field.Comment.Text())
func (sp *StructParser) fieldAcronym(field *ast.Field) bool {
return sp.flagAcronymRegex.MatchString(field.Comment.Text())
}

func (sp *StructParser) constructorFlags(st *ast.StructType) StructFlags {
Expand Down
Loading

0 comments on commit 8544d75

Please sign in to comment.