From 8544d75606c579eb2fde7bdd9ca5f21d4a3235f2 Mon Sep 17 00:00:00 2001 From: Toly Pochkin Date: Tue, 7 Dec 2021 17:14:13 -0800 Subject: [PATCH] add field acronym support --- README.md | 107 ++++++++++++++++++++++++------------------------ example/main.go | 5 +-- gobld.go | 57 ++++++++++++++++++-------- main.go | 32 +++++++++------ 4 files changed, 115 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 97bf3ba..87a5dc1 100644 --- a/README.md +++ b/README.md @@ -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 { @@ -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 { @@ -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 @@ -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 @@ -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:_ } @@ -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 ./... @@ -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 task. Name it `Go Generate files` and setup **Files type**: `Go files`, **Program**: `go`, **Arguments**: `generate`.
At this point it should work, but File Watcher will be monitoring your entire project directory and not only your own @@ -171,23 +173,22 @@ This will do it. Now when you save Go file - `go generate` will be automatically `-input ` - input file name where to read structures from -`-output ` - 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 ` - 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: diff --git a/example/main.go b/example/main.go index 618fe81..ddc4274 100644 --- a/example/main.go +++ b/example/main.go @@ -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 { diff --git a/gobld.go b/gobld.go index 0adcef5..57e0d37 100644 --- a/gobld.go +++ b/gobld.go @@ -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 { @@ -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 { @@ -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) @@ -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) @@ -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`), } } @@ -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 { diff --git a/main.go b/main.go index 6ac5062..700dbde 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ func parseCommandLineArgs() ( flag.Parse() if isFlagPassed("print-version") { - println("gobetter version 0.7") + println("gobetter version 0.8") } inFilename = *inputFilePtr @@ -93,11 +93,12 @@ func parseCommandLineArgs() ( os.Exit(1) } - if *receiverTypePtr == "pointer" { + switch { + case *receiverTypePtr == "pointer": usePtrReceiver = true - } else if *receiverTypePtr == "value" { + case *receiverTypePtr == "value": usePtrReceiver = false - } else { + default: _, _ = fmt.Fprintln(os.Stderr, "Error: \"receiver\" flag must be \"pointer\" or \"value\"") os.Exit(1) } @@ -127,7 +128,11 @@ func isFlagPassed(name string) bool { func main() { inFilename, outFilename, defaultTypes, usePtrReceiver, constructorVisibility := parseCommandLineArgs() - fileContent, err := ioutil.ReadFile(inFilename) + fileContent, err := os.ReadFile(inFilename) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "error: failed to read file %s: %v\n", inFilename, err) + os.Exit(1) + } fset := token.NewFileSet() astFile, err := parser.ParseFile(fset, inFilename, nil, parser.ParseComments) if err != nil { @@ -164,11 +169,12 @@ func main() { } structFlags.ProcessStruct = true structFlags.PtrReceiver = usePtrReceiver - if constructorVisibility == "exported" { + switch { + case constructorVisibility == "exported": structFlags.Visibility = ExportedVisibility - } else if constructorVisibility == "package" { + case constructorVisibility == "package": structFlags.Visibility = PackageLevelVisibility - } else { + default: structFlags.Visibility = NoVisibility } } @@ -178,20 +184,20 @@ func main() { for _, field := range st.Fields.List { fieldTypeText := sp.fieldTypeText(field) for _, fieldName := range field.Names { + acronym := sp.fieldAcronym(field) if structFlags.Visibility != NoVisibility { if !sp.fieldOptional(field) { - structArgName := gobBld.appendArgStruct(structName, fieldName.Name, fieldTypeText, structFlags) + structArgName := gobBld.appendArgStruct(structName, fieldName.Name, fieldTypeText, + structFlags, acronym) if gobBld.constructorValueDef.Len() == 0 { gobBld.appendBeginConstructorDef(structName, structFlags) gobBld.appendBeginConstructorBody(structName) } - gobBld.appendConstructorArg(fieldName.Name, structArgName) + gobBld.appendConstructorArg(fieldName.Name, structArgName, acronym) } } if sp.fieldGetter(field) { - gobBld.appendGetter(structName, fieldName.Name, fieldTypeText, structFlags, false) - } else if sp.fieldUppercaseGetter(field) { - gobBld.appendGetter(structName, fieldName.Name, fieldTypeText, structFlags, true) + gobBld.appendGetter(structName, fieldName.Name, fieldTypeText, structFlags, acronym) } } }