├── .github └── workflows │ └── golangci-lint.yml ├── .golangci.yml ├── LICENSE ├── README.md ├── examples ├── codegen │ ├── paginator.go │ ├── settable.go │ └── sorter.go ├── go.mod ├── go.sum ├── main.go └── my_project │ ├── models │ ├── models.go │ ├── paginator_gen.go │ └── settable-input_gen.go │ └── responses │ ├── sort-by-keys_gen.go │ └── structs.go ├── go.mod ├── go.sum ├── simplegen.go ├── templates.go └── types.go /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | # pull-requests: read 13 | 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-go@v4 21 | with: 22 | go-version-file: go.mod 23 | cache: false 24 | - name: golangci-lint 25 | uses: golangci/golangci-lint-action@v3 26 | with: 27 | # Require: The version of golangci-lint to use. 28 | # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. 29 | # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. 30 | version: v1.54 31 | 32 | # Optional: working directory, useful for monorepos 33 | # working-directory: somedir 34 | 35 | # Optional: golangci-lint command line arguments. 36 | # 37 | # Note: By default, the `.golangci.yml` file should be at the root of the repository. 38 | # The location of the configuration file can be changed by using `--config=` 39 | # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 40 | 41 | # Optional: show only new issues if it's a pull request. The default value is `false`. 42 | # only-new-issues: true 43 | 44 | # Optional: if set to true, then all caching functionality will be completely disabled, 45 | # takes precedence over all other caching options. 46 | # skip-cache: true 47 | 48 | # Optional: if set to true, then the action won't cache or restore ~/go/pkg. 49 | # skip-pkg-cache: true 50 | 51 | # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. 52 | # skip-build-cache: true 53 | 54 | # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. 55 | # install-mode: "goinstall" 56 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # which dirs to skip: issues from them won't be reported; 7 | # can use regexp here: generated.*, regexp is applied on full path; 8 | # default value is empty list, but default dirs are skipped independently 9 | # from this option's value (see skip-dirs-use-default). 10 | # skip-dirs: 11 | # - 12 | 13 | 14 | # which files to skip: they will be analyzed, but issues from them 15 | # won't be reported. Default value is empty list, but there is 16 | # no need to include all autogenerated files, we confidently recognize 17 | # autogenerated files. If it's not please let us know. 18 | # skip-files: 19 | # - common.resolvers.go 20 | 21 | tests: true 22 | 23 | 24 | # all available settings of specific linters 25 | linters-settings: 26 | errcheck: 27 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 28 | # default is false: such cases aren't reported by default. 29 | check-type-assertions: false 30 | 31 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 32 | # default is false: such cases aren't reported by default. 33 | check-blank: true 34 | 35 | # [deprecated] comma-separated list of pairs of the form pkg:regex 36 | # the regex is used to ignore names within pkg. (default "fmt:.*"). 37 | # see https://github.com/kisielk/errcheck#the-deprecated-method for details 38 | ignore: fmt:.*,io/ioutil:^Read.* 39 | 40 | gofumpt: 41 | module-path: hrm 42 | 43 | govet: 44 | # report about shadowed variables 45 | check-shadowing: true 46 | 47 | # settings per analyzer 48 | settings: 49 | printf: # analyzer name, run `go tool vet help` to see all analyzers 50 | funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 51 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 52 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 53 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 54 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 55 | 56 | # enable or disable analyzers by name 57 | enable: 58 | - atomicalign 59 | # - fieldalignment 60 | enable-all: false 61 | disable: 62 | - shadow 63 | disable-all: false 64 | golint: 65 | # minimal confidence for issues, default is 0.8 66 | min-confidence: 0.8 67 | gofmt: 68 | # simplify code: gofmt with `-s` option, true by default 69 | simplify: true 70 | gocyclo: 71 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 72 | min-complexity: 15 73 | gocognit: 74 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 75 | min-complexity: 30 76 | dupl: 77 | # tokens count to trigger issue, 150 by default 78 | threshold: 100 79 | goconst: 80 | # minimal length of string constant, 3 by default 81 | min-len: 3 82 | # minimal occurrences count to trigger, 3 by default 83 | min-occurrences: 3 84 | misspell: 85 | # Correct spellings using locale preferences for US or UK. 86 | # Default is to use a neutral variety of English. 87 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 88 | locale: US 89 | ignore-words: 90 | - someword 91 | lll: 92 | # max line length, lines longer will be reported. Default is 120. 93 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 94 | line-length: 160 95 | # tab width in spaces. Default to 1. 96 | tab-width: 1 97 | unused: 98 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 99 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 100 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 101 | # with golangci-lint call it on a directory with the changed file. 102 | check-exported: false 103 | unparam: 104 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 105 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 106 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 107 | # with golangci-lint call it on a directory with the changed file. 108 | check-exported: false 109 | nakedret: 110 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 111 | max-func-lines: 30 112 | prealloc: 113 | # XXX: we don't recommend using this linter before doing performance profiling. 114 | # For most programs usage of prealloc will be a premature optimization. 115 | 116 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 117 | # True by default. 118 | simple: true 119 | range-loops: true # Report preallocation suggestions on range loops, true by default 120 | for-loops: false # Report preallocation suggestions on for loops, false by default 121 | gocritic: 122 | # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 123 | disabled-checks: 124 | - regexpMust 125 | - rangeValCopy 126 | - importShadow 127 | - docStub 128 | 129 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 130 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 131 | enabled-tags: 132 | - performance 133 | - style 134 | - diagnostic 135 | - experimental 136 | - opinionated 137 | settings: # settings passed to gocritic 138 | captLocal: # must be valid enabled check name 139 | paramsOnly: true 140 | hugeParam: 141 | sizeThreshold: 1024 142 | 143 | godox: 144 | # report any comments starting with keywords, this is useful for TODO or FIXME comments that 145 | # might be left in the code accidentally and should be resolved before merging 146 | keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting 147 | - NOTE 148 | - OPTIMIZE # marks code that should be optimized before merging 149 | - HACK # marks hack-arounds that should be removed before merging 150 | dogsled: 151 | # checks assignments with too many blank identifiers; default is 2 152 | max-blank-identifiers: 2 153 | 154 | whitespace: 155 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 156 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 157 | 158 | issues: 159 | # Excluding configuration per-path, per-linter, per-text and per-source 160 | exclude-rules: 161 | # Exclude some linters from running on tests files. 162 | # - path: resolvers\.go 163 | # text: "typeDefFirst:" 164 | # linters: 165 | # - gocritic 166 | # - path: resolvers\.go 167 | # text: "paramTypeCombine:" 168 | # linters: 169 | # - gocritic 170 | # - path: resolvers\.go 171 | # linters: 172 | # - gofumpt 173 | - path: _test\.go 174 | linters: 175 | - bodyclose 176 | - errcheck 177 | - forcetypeassert 178 | - dogsled 179 | - contextcheck 180 | - containedctx 181 | - errorlint 182 | - gomnd 183 | 184 | linters: 185 | enable-all: true 186 | disable: 187 | - cyclop #: checks function and package cyclomatic complexity [fast: false, auto-fix: false] 188 | - gocyclo # checks function and package cyclomatic complexity 189 | - gocognit # checks function and package cyclomatic complexity 190 | - funlen # checks length of functions 191 | - deadcode # [deprecated]: Finds unused code [fast: false, auto-fix: false] 192 | - decorder #: check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false] 193 | - depguard #: Go linter that checks if package imports are in a list of acceptable packages. 194 | - exhaustivestruct # [deprecated]: Checks if all struct's fields are initialized [fast: false, auto-fix: false] 195 | - exhaustruct # Checks if all struct's fields are initialized 196 | - gochecknoglobals #: check that no global variables exist [fast: false, auto-fix: false] 197 | - gochecknoinits #: Checks that no init functions are present in Go code [fast: true, auto-fix: false] 198 | - godox #: Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] 199 | - goerr113 # Forces to use package errors instead of in-place errors 200 | - gomnd # magic numbers 201 | - golint # [deprecated]: Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: false, auto-fix: false] 202 | - interfacer # [deprecated]: Linter that suggests narrower interface types [fast: false, auto-fix: false] 203 | - ifshort # [deprecated]: 204 | - maligned # [deprecated]: Tool to detect Go structs that would take less memory if their fields were sorted [fast: false, auto-fix: false] 205 | - nlreturn # blank lines before return 206 | - nonamedreturns #: Reports all named returns [fast: false, auto-fix: false] 207 | - nosnakecase # [deprecated]: nosnakecase is a linter that detects snake case of variable naming and function name. [fast: true, auto-fix: false] 208 | - paralleltest # forces to use t.Parallel() 209 | - scopelint # [deprecated]: Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] 210 | - structcheck # [deprecated]: Finds unused struct fields [fast: false, auto-fix: false] 211 | - testpackage # forces to use package_test naming 212 | - varcheck # [deprecated]: Finds unused global variables and constants [fast: false, auto-fix: false] 213 | - varnamelen 214 | - wrapcheck # forces to wrap external package errors 215 | - wsl # cuddles, Whitespace Linter - Forces you to use empty lines!. 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alwx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simplegen 2 | Tool for simple use of ast-based code generation. 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/AlwxSin/simplegen.svg)](https://pkg.go.dev/github.com/AlwxSin/simplegen) 5 | 6 | ### Problem 7 | In large codebase or in microservice architecture could be a bunch of codegen tools on many files. There often runs via `go:generate`. 8 | Which leads to slowdown of build process, because each call to `go:generate` means that tool should parse file (slow), build `ast.Tree` (slow), do some stuff with `ast` (usually fast) and write new file (slow). 9 | 10 | ### Solution 11 | `simplegen` do all slow stuff **only once** allowing developer to use `ast` to collect data for codegen templates. 12 | 13 | ## Usage 14 | ```go 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "go/ast" 20 | "golang.org/x/tools/go/packages" 21 | 22 | "github.com/AlwxSin/simplegen" 23 | ) 24 | 25 | var PaginatorTemplate = ` 26 | {{ range $key, $struct := .Specs }} 27 | // {{$struct.Name}}ListPaginated represents {{$struct.Name}} list in a pagination container. 28 | type {{$struct.Name}}ListPaginated struct { 29 | CurrentCursor *string ` + "`json:\"currentCursor\"`\n" + 30 | ` NextCursor *string ` + "`json:\"nextCursor\"`\n" + 31 | ` Results []*{{$struct.Name}} ` + "`json:\"results\"`\n" + 32 | ` 33 | isPaginated bool 34 | limit int 35 | offset int 36 | } 37 | ` 38 | 39 | func Paginator( 40 | sg *simplegen.SimpleGenerator, 41 | pkg *packages.Package, 42 | node *ast.TypeSpec, 43 | comment *ast.Comment, 44 | ) (templateData simplegen.SpecData, imports []string, err error) { 45 | imports = append(imports, "strconv") 46 | 47 | type PaginatorTypeSpec struct { 48 | Name string 49 | } 50 | 51 | tmplData := &PaginatorTypeSpec{ 52 | Name: node.Name.Name, 53 | } 54 | return simplegen.SpecData(tmplData), imports, nil 55 | } 56 | 57 | // We want all our structs to be paginated in same way, but don't want to repeat boilerplate code for pagination. 58 | // Use magic comment 59 | // simplegen:{generator_name} 60 | 61 | // simplegen:paginator 62 | type User struct { 63 | ID int 64 | Email string 65 | } 66 | 67 | // simplegen:paginator 68 | type Work struct { 69 | ID int 70 | UserID int 71 | } 72 | 73 | func main() { 74 | var pn simplegen.PackageNames // it's an alias for []string type, need for multiple cli arguments 75 | 76 | flag.Var(&pn, "package", "Package where simplegen should find magic comments") 77 | flag.Parse() 78 | 79 | // create simplegen instance with function map 80 | // {generator_name} -> { 81 | // Template -> string with template 82 | // GeneratorFunc -> func to extract data from ast.Node 83 | // } 84 | sg, _ := simplegen.NewSimpleGenerator(pn, simplegen.GeneratorsMap{ 85 | "paginator": simplegen.TemplateGenerator{ 86 | Template: PaginatorTemplate, 87 | GeneratorFunc: Paginator, 88 | }, 89 | }, nil) 90 | _ = sg.Generate() 91 | } 92 | ``` 93 | And run it with 94 | ```shell 95 | go run main.go -package github.com/my_project/main 96 | ``` 97 | `-package` argument is required and should be in form of `{module_in_go.mod}/{path}/{package}` 98 | 99 | It can be used by `go:generate` as well 100 | 101 | ```go 102 | //go:generate go run github.com/AlwxSin/simplegen -package github.com/my_project/responses -package github.com/my_project/models 103 | package main 104 | 105 | func main() { 106 | cmd.Execute() 107 | } 108 | ``` 109 | 110 | ### Documentation 111 | 112 | See [godoc][godoc] for general API details. 113 | See [examples](examples) for more ways to use `simplegen`. 114 | 115 | ### Tools 116 | `simplegen` instance has several useful methods to work with `ast`. For example, we have the following struct 117 | ```go 118 | package models 119 | 120 | import "my_project/types" 121 | 122 | // simplegen:my_command 123 | type MyStruct struct { 124 | ID int 125 | Settings types.JSONB 126 | } 127 | ``` 128 | And we want to generate some struct based on `MyStruct` fields. 129 | ```go 130 | // Code generated by github.com/AlwxSin/simplegen, DO NOT EDIT. 131 | package models 132 | 133 | import "time" 134 | import "my_project/types" 135 | 136 | type MyStructGenerated struct { 137 | // all fields from base MyStruct 138 | ID int 139 | Settings types.JSONB 140 | // extra fields 141 | CreatedAt time.Time 142 | } 143 | ``` 144 | Our `generator` function has `pkg` in arguments, but it's a `models` package. 145 | When we parse `MyStruct` fields: 146 | 1. We need `StructType` of `MyStruct` to iterate over struct fields. 147 | 2. We should process `Settings` field with external `JSONB` type, we should know which package contains this type to import it in generated file. 148 | ```go 149 | package models 150 | 151 | import ( 152 | "strings" 153 | "strconv" 154 | "github.com/AlwxSin/simplegen" 155 | "golang.org/x/tools/go/packages" 156 | "go/ast" 157 | "fmt" 158 | "go/types" 159 | ) 160 | 161 | func MyCommandGenerator( 162 | sg *simplegen.SimpleGenerator, 163 | pkg *packages.Package, 164 | node *ast.TypeSpec, 165 | comment *ast.Comment, 166 | ) (templateData simplegen.SpecData, imports []string, err error) { 167 | // 1. Get StructType of MyStruct 168 | structType, _ := sg.GetStructType(pkg, node.Name.Name) 169 | for i := 0; i < structType.NumFields(); i++ { 170 | field := structType.Field(i) 171 | // 2. get package of MyStruct field 172 | pkgPath := field.Type().(*types.Named).Obj().Pkg().Path() 173 | fieldPkg, _ := sg.GetPackage(pkgPath) 174 | fmt.Println(fieldPkg) 175 | } 176 | } 177 | ``` 178 | 179 | 180 | [godoc]: https://pkg.go.dev/github.com/AlwxSin/simplegen "Documentation on godoc" -------------------------------------------------------------------------------- /examples/codegen/paginator.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "github.com/AlwxSin/simplegen" 5 | "go/ast" 6 | "golang.org/x/tools/go/packages" 7 | ) 8 | 9 | var PaginatorTemplate = ` 10 | {{ range $key, $struct := .Specs }} 11 | // {{$struct.Name}}ListPaginated represents {{$struct.Name}} list in a pagination container. 12 | type {{$struct.Name}}ListPaginated struct { 13 | CurrentCursor *string ` + "`json:\"currentCursor\"`\n" + 14 | ` NextCursor *string ` + "`json:\"nextCursor\"`\n" + 15 | ` Results []*{{$struct.Name}} ` + "`json:\"results\"`\n" + 16 | ` 17 | isPaginated bool 18 | limit int 19 | offset int 20 | } 21 | 22 | // New{{$struct.Name}}ListPaginated returns paginated {{$struct.Name}} list if able to parse PaginateOptions. 23 | func New{{$struct.Name}}ListPaginated(paginateOptions PaginateOptions) (*{{$struct.Name}}ListPaginated, error) { 24 | offset := 0 25 | if paginateOptions.Cursor != nil { 26 | o, err := strconv.Atoi(*paginateOptions.Cursor) 27 | if err != nil { 28 | return nil, err 29 | } 30 | offset = o 31 | } 32 | return &{{$struct.Name}}ListPaginated{ 33 | Results: make([]*{{$struct.Name}}, 0), 34 | CurrentCursor: paginateOptions.Cursor, 35 | isPaginated: paginateOptions.IsPaginated(), 36 | limit: paginateOptions.Limit, 37 | offset: offset, 38 | }, nil 39 | } 40 | 41 | {{ end }} 42 | ` 43 | 44 | func Paginator( 45 | sg *simplegen.SimpleGenerator, 46 | pkg *packages.Package, 47 | node *ast.TypeSpec, 48 | comment *ast.Comment, 49 | ) (templateData simplegen.SpecData, imports []string, err error) { 50 | imports = append(imports, "strconv") 51 | 52 | type PaginatorTypeSpec struct { 53 | Name string 54 | } 55 | 56 | tmplData := &PaginatorTypeSpec{ 57 | Name: node.Name.Name, 58 | } 59 | return simplegen.SpecData(tmplData), imports, nil 60 | } 61 | -------------------------------------------------------------------------------- /examples/codegen/settable.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "fmt" 5 | "github.com/AlwxSin/simplegen" 6 | "go/ast" 7 | "go/types" 8 | "golang.org/x/tools/go/packages" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | var SettableTemplate = ` 14 | // Settable acts like sql.NullString, sql.NullInt64 but generic. 15 | // It allows to define was value set or it's zero value. 16 | type Settable[T any] struct { 17 | Value T 18 | IsSet bool 19 | } 20 | 21 | // NewSettable returns set value. 22 | func NewSettable[T any](value T) Settable[T] { 23 | return Settable[T]{ 24 | Value: value, 25 | IsSet: true, 26 | } 27 | } 28 | 29 | {{ range $index, $struct := .Specs }} 30 | // {{$struct.Name}}Settable allows to use {{$struct.Name}} with Settable fields 31 | type {{$struct.Name}}Settable struct { 32 | {{- range $index, $field := $struct.Fields }} 33 | {{$field.Name}} Settable[{{$field.TypeName}}] {{formatSettableTags $field.Tags}} 34 | {{- end }} 35 | } 36 | 37 | func (inp *{{$struct.Name}}) ToSettable(inputFields map[string]interface{}) *{{$struct.Name}}Settable { 38 | settable := &{{$struct.Name}}Settable{} 39 | {{ range $index, $field := $struct.Fields }} 40 | if _, ok := inputFields["{{$field.JSONTag}}"]; ok { 41 | settable.{{$field.Name}} = NewSettable(inp.{{$field.Name}}) 42 | } 43 | {{end}} 44 | return settable 45 | } 46 | 47 | {{ end }} 48 | ` 49 | 50 | type InputField struct { 51 | Name, TypeName, Tags, JSONTag string 52 | } 53 | 54 | type InputToSettableSpecData struct { 55 | Name string 56 | Fields []*InputField 57 | } 58 | 59 | func Settable( 60 | sg *simplegen.SimpleGenerator, 61 | pkg *packages.Package, 62 | node *ast.TypeSpec, 63 | comment *ast.Comment, 64 | ) (templateData simplegen.SpecData, imports []string, err error) { 65 | return parseSettableStruct(sg, pkg, node.Name.Name) 66 | } 67 | 68 | func parseSettableStruct( 69 | sg *simplegen.SimpleGenerator, 70 | pkg *packages.Package, 71 | structName string, 72 | ) (specData *InputToSettableSpecData, imports []string, err error) { 73 | structType, err := sg.GetStructType(pkg, structName) 74 | if err != nil { 75 | return nil, nil, err 76 | } 77 | 78 | specData = &InputToSettableSpecData{Name: structName} 79 | 80 | // iterate over struct fields 81 | for i := 0; i < structType.NumFields(); i++ { 82 | field := structType.Field(i) 83 | 84 | // check if field is embedded 85 | // if so, extract field type from package 86 | // WARNING, there is no check if field type from another package, this is just an example 87 | if field.Embedded() { 88 | // field is embedded (usually common fields like ID or CreatedAt), go recursive 89 | embedInput, embedImports, tagErr := parseSettableStruct(sg, pkg, field.Name()) 90 | if tagErr != nil { 91 | return nil, nil, tagErr 92 | } 93 | if len(embedInput.Fields) > 0 { 94 | specData.Fields = append(specData.Fields, embedInput.Fields...) 95 | } 96 | if len(embedImports) > 0 { 97 | imports = append(imports, embedImports...) 98 | } 99 | continue 100 | } 101 | 102 | // extract tags to use in settable structure 103 | tagValue := structType.Tag(i) 104 | jsonTagValue := parseJSONOrYamlTag(tagValue) 105 | if jsonTagValue == "" { 106 | return nil, nil, fmt.Errorf("type %v: field %s should has json/yaml value", structType, field.Name()) 107 | } 108 | 109 | // store field type to use in settable type and collect all imports 110 | fieldType, fieldImports := extractFieldInfo(field.Type(), pkg) 111 | if fieldType != "" { 112 | specData.Fields = append(specData.Fields, &InputField{ 113 | Name: field.Name(), 114 | TypeName: fieldType, 115 | Tags: tagValue, 116 | JSONTag: jsonTagValue, 117 | }) 118 | } 119 | if len(fieldImports) > 0 { 120 | 121 | imports = append(imports, fieldImports...) 122 | } 123 | } 124 | 125 | return specData, imports, nil 126 | } 127 | 128 | // extractFieldInfo returns type and imports 129 | // time.Time, []string{time} 130 | // models.User, []string{examples/my_project/models} 131 | // *models.User, []string{examples/my_project/models} 132 | // string, []string{}. 133 | func extractFieldInfo(field types.Type, curPkg *packages.Package) (fieldTypeName string, fieldImports []string) { 134 | switch v := field.(type) { 135 | case *types.Named: 136 | typeName, fieldImport := getFieldInfo(v, curPkg) 137 | var imports []string 138 | if fieldImport != "" { 139 | imports = []string{fieldImport} 140 | } 141 | return typeName, imports 142 | case *types.Basic: 143 | return v.Name(), []string{} 144 | case *types.Interface: 145 | return v.String(), []string{} 146 | case *types.Pointer: 147 | typeName, imports := extractFieldInfo(v.Elem(), curPkg) 148 | return fmt.Sprintf("*%s", typeName), imports 149 | case *types.Slice: 150 | typeName, imports := extractFieldInfo(v.Elem(), curPkg) 151 | return fmt.Sprintf("[]%s", typeName), imports 152 | case *types.Map: 153 | keyInfo, keyImports := extractFieldInfo(v.Key(), curPkg) 154 | elemInfo, elemImports := extractFieldInfo(v.Elem(), curPkg) 155 | return fmt.Sprintf("map[%s]%s", keyInfo, elemInfo), append(keyImports, elemImports...) 156 | default: 157 | return "", nil 158 | } 159 | } 160 | 161 | // getFieldInfo returns type definition and import path related to package 162 | // "models.User", "examples/my_project/models". 163 | func getFieldInfo(field *types.Named, pkg *packages.Package) (fieldTypeName, fieldImport string) { 164 | if field.Obj().Pkg().Path() == pkg.PkgPath { 165 | return field.Obj().Name(), "" 166 | } 167 | return field.Obj().Pkg().Name() + "." + field.Obj().Name(), field.Obj().Pkg().Path() 168 | } 169 | 170 | // parseJSONOrYamlTag extract json/yaml value from tags 171 | // `yaml:"phoneYaml" json:"phone"` - > "phone" 172 | // `yaml:"phoneYaml"` - > "phoneYaml" 173 | // `db:"phone"` - > "". 174 | func parseJSONOrYamlTag(rawTags string) string { 175 | if rawTags == "" { 176 | return "" 177 | } 178 | tags := strings.Split(rawTags, " ") 179 | 180 | jsonTag := "" 181 | yamlTag := "" 182 | for _, tag := range tags { 183 | hasJSONTag := strings.Contains(tag, "json:") 184 | hasYamlTag := strings.Contains(tag, "yaml:") 185 | if !hasJSONTag && !hasYamlTag { 186 | continue 187 | } 188 | if hasJSONTag { 189 | t, qErr := strconv.Unquote(tag[5:]) 190 | if qErr != nil { 191 | return "" 192 | } 193 | jsonTag = strings.Split(t, ",")[0] 194 | continue 195 | } 196 | if hasYamlTag { 197 | t, qErr := strconv.Unquote(tag[5:]) 198 | if qErr != nil { 199 | return "" 200 | } 201 | yamlTag = strings.Split(t, ",")[0] 202 | continue 203 | } 204 | } 205 | if jsonTag != "" && jsonTag != "-" { 206 | return jsonTag 207 | } 208 | if yamlTag != "" && yamlTag != "-" { 209 | return yamlTag 210 | } 211 | return "" 212 | } 213 | 214 | func FormatSettableTags(tag string) string { 215 | if tag == "" { 216 | return tag 217 | } 218 | return fmt.Sprintf("`%s`", tag) 219 | } 220 | -------------------------------------------------------------------------------- /examples/codegen/sorter.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/AlwxSin/simplegen" 7 | "go/ast" 8 | "golang.org/x/tools/go/packages" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var SorterTemplate = ` 14 | {{ range $key, $spec := .Specs }} 15 | func {{$spec.Type.Name}}List{{$spec.Suffix}}SortByKeys(vs {{if not $spec.Type.IsSlice}}[]{{end}}{{$spec.Type}}, keys []{{$spec.FieldType}}) []{{$spec.Type}} { 16 | res := make([]{{$spec.Type}}, len(keys)) 17 | for i, key := range keys { 18 | var appendable {{$spec.Type}} 19 | for _, v := range vs { 20 | if key == {{if $spec.FieldIsPtr}}*{{end}}v.{{$spec.FieldName}} { 21 | {{- if $spec.Type.IsSlice}} 22 | appendable = append(appendable, v) 23 | {{- else}} 24 | appendable = v 25 | break 26 | {{- end}} 27 | } 28 | } 29 | res[i] = appendable 30 | } 31 | return res 32 | } 33 | 34 | {{end}} 35 | ` 36 | 37 | func Sorter( 38 | sg *simplegen.SimpleGenerator, 39 | pkg *packages.Package, 40 | node *ast.TypeSpec, 41 | comment *ast.Comment, 42 | ) (templateData simplegen.SpecData, imports []string, err error) { 43 | var ( 44 | typeName string 45 | fieldName string 46 | fieldType string 47 | suffix string 48 | ) 49 | 50 | fs := flag.FlagSet{} 51 | fs.StringVar(&typeName, "type", "", "Type for which need to generate") 52 | fs.StringVar(&suffix, "suffix", "", "Suffix for generated function") 53 | fs.StringVar(&fieldName, "fieldName", "ID", "Field name which be used as identifier") 54 | fs.StringVar(&fieldType, "fieldType", "int", "Field type") 55 | 56 | s := strings.TrimPrefix(comment.Text, "//") 57 | s = strings.TrimSpace(s) 58 | s = strings.TrimPrefix(s, simplegen.CmdKey+":sort-by-keys ") 59 | args := strings.Split(s, " ") 60 | 61 | err = fs.Parse(args) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | 66 | parts := partsRe.FindStringSubmatch(typeName) 67 | if len(parts) != 4 { 68 | return nil, nil, fmt.Errorf("type must be in the form []*github.com/import/path.Name") 69 | } 70 | 71 | t := &goType{ 72 | Modifiers: parts[1], 73 | ImportPath: parts[2], 74 | Name: strings.TrimPrefix(parts[3], "."), 75 | } 76 | 77 | if t.Name == "" { 78 | t.Name = t.ImportPath 79 | t.ImportPath = "" 80 | } else { 81 | imports = append(imports, t.ImportPath) 82 | } 83 | 84 | if t.ImportPath != "" { 85 | typePkg, err := sg.GetPackage(t.ImportPath) 86 | if err != nil { 87 | return nil, nil, err 88 | } 89 | t.ImportName = typePkg.Name 90 | } 91 | 92 | ssd := &SorterSpecData{ 93 | Type: t, 94 | Suffix: suffix, 95 | FieldType: fieldType, 96 | FieldName: fieldName, 97 | } 98 | 99 | typePkg, err := sg.GetPackage(t.ImportPath) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | structType, err := sg.GetStructType(typePkg, t.Name) 105 | if err != nil { 106 | return nil, nil, err 107 | } 108 | 109 | for i := 0; i < structType.NumFields(); i++ { 110 | field := structType.Field(i) 111 | if field.Name() != fieldName { 112 | continue 113 | } 114 | if field.Type().String()[0] == '*' { 115 | ssd.FieldIsPtr = true 116 | } 117 | break 118 | } 119 | return ssd, imports, nil 120 | } 121 | 122 | type SorterSpecData struct { 123 | Type *goType 124 | FieldType string 125 | FieldName string 126 | Suffix string 127 | FieldIsPtr bool 128 | } 129 | 130 | type goType struct { 131 | Modifiers string 132 | ImportPath string 133 | ImportName string 134 | Name string 135 | } 136 | 137 | func (t *goType) IsSlice() bool { 138 | return strings.HasPrefix(t.Modifiers, "[]") 139 | } 140 | 141 | func (t *goType) String() string { 142 | if t.ImportName != "" { 143 | return t.Modifiers + t.ImportName + "." + t.Name 144 | } 145 | 146 | return t.Modifiers + t.Name 147 | } 148 | 149 | var partsRe = regexp.MustCompile(`^([\[\]\*]*)(.*?)(\.\w*)?$`) 150 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module examples 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/AlwxSin/simplegen v0.1.0 7 | golang.org/x/tools v0.13.0 8 | ) 9 | 10 | require ( 11 | golang.org/x/mod v0.12.0 // indirect 12 | golang.org/x/sys v0.12.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlwxSin/simplegen v0.1.0 h1:UrOITwwgbL5Fok1HqrH9cZrus7fiu9L7OKVB+H8S0NA= 2 | github.com/AlwxSin/simplegen v0.1.0/go.mod h1:Vp8lvz2TUrw0qrJkrduaj/wRpuqBPtJDZ2uxa7mfi0U= 3 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 4 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 5 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 6 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 7 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 8 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 9 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 10 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 11 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "examples/codegen" 5 | "flag" 6 | "fmt" 7 | "github.com/AlwxSin/simplegen" 8 | "text/template" 9 | ) 10 | 11 | // main 12 | // Example 13 | // go run main.go -package examples/my_project/models -package examples/my_project/responses 14 | func main() { 15 | var ( 16 | help bool 17 | pn simplegen.PackageNames 18 | ) 19 | 20 | flag.BoolVar(&help, "h", false, "Show this help text") 21 | flag.BoolVar(&help, "help", false, "") 22 | flag.Var(&pn, "package", "Package where simplegen should find magic comments") 23 | flag.Parse() 24 | 25 | if help { 26 | flag.PrintDefaults() 27 | return 28 | } 29 | 30 | pn = simplegen.PackageNames{"examples/my_project/models", "examples/my_project/responses"} 31 | 32 | sg, err := simplegen.NewSimpleGenerator(pn, simplegen.GeneratorsMap{ 33 | "paginator": simplegen.TemplateGenerator{ 34 | Template: codegen.PaginatorTemplate, 35 | GeneratorFunc: codegen.Paginator, 36 | }, 37 | "settable-input": simplegen.TemplateGenerator{ 38 | Template: codegen.SettableTemplate, 39 | GeneratorFunc: codegen.Settable, 40 | }, 41 | "sort-by-keys": simplegen.TemplateGenerator{ 42 | Template: codegen.SorterTemplate, 43 | GeneratorFunc: codegen.Sorter, 44 | }, 45 | }, template.FuncMap{ 46 | "formatSettableTags": codegen.FormatSettableTags, 47 | }) 48 | if err != nil { 49 | fmt.Println(err) 50 | return 51 | } 52 | 53 | err = sg.Generate() 54 | if err != nil { 55 | fmt.Println(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/my_project/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type JSONB map[string]interface{} 6 | 7 | type Common struct { 8 | ID int `json:"id" yaml:"id"` 9 | CreatedAt *time.Time `json:"createdAt" yaml:"createdAt"` 10 | } 11 | 12 | // simplegen:settable-input 13 | // simplegen:paginator 14 | type User struct { 15 | Common 16 | FirstName string `json:"firstName" yaml:"firstName"` 17 | Email string `json:"email" yaml:"email"` 18 | Age int `json:"age" yaml:"age"` 19 | Settings JSONB `json:"settings" yaml:"settings"` 20 | } 21 | 22 | // PaginateOptions describes pagination. 23 | type PaginateOptions struct { 24 | Cursor *string `json:"cursor"` 25 | Limit int `json:"limit"` 26 | NoPagination bool `json:"noPagination"` 27 | } 28 | 29 | // IsPaginated returns if user requires pagination. 30 | func (p PaginateOptions) IsPaginated() bool { 31 | return !p.NoPagination 32 | } 33 | -------------------------------------------------------------------------------- /examples/my_project/models/paginator_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/AlwxSin/simplegen, DO NOT EDIT. 2 | package models 3 | 4 | import ( 5 | "strconv" 6 | ) 7 | 8 | // UserListPaginated represents User list in a pagination container. 9 | type UserListPaginated struct { 10 | CurrentCursor *string `json:"currentCursor"` 11 | NextCursor *string `json:"nextCursor"` 12 | Results []*User `json:"results"` 13 | 14 | isPaginated bool 15 | limit int 16 | offset int 17 | } 18 | 19 | // NewUserListPaginated returns paginated User list if able to parse PaginateOptions. 20 | func NewUserListPaginated(paginateOptions PaginateOptions) (*UserListPaginated, error) { 21 | offset := 0 22 | if paginateOptions.Cursor != nil { 23 | o, err := strconv.Atoi(*paginateOptions.Cursor) 24 | if err != nil { 25 | return nil, err 26 | } 27 | offset = o 28 | } 29 | return &UserListPaginated{ 30 | Results: make([]*User, 0), 31 | CurrentCursor: paginateOptions.Cursor, 32 | isPaginated: paginateOptions.IsPaginated(), 33 | limit: paginateOptions.Limit, 34 | offset: offset, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /examples/my_project/models/settable-input_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/AlwxSin/simplegen, DO NOT EDIT. 2 | package models 3 | 4 | import ( 5 | "time" 6 | ) 7 | 8 | // Settable acts like sql.NullString, sql.NullInt64 but generic. 9 | // It allows to define was value set or it's zero value. 10 | type Settable[T any] struct { 11 | Value T 12 | IsSet bool 13 | } 14 | 15 | // NewSettable returns set value. 16 | func NewSettable[T any](value T) Settable[T] { 17 | return Settable[T]{ 18 | Value: value, 19 | IsSet: true, 20 | } 21 | } 22 | 23 | // UserSettable allows to use User with Settable fields 24 | type UserSettable struct { 25 | ID Settable[int] `json:"id" yaml:"id"` 26 | CreatedAt Settable[*time.Time] `json:"createdAt" yaml:"createdAt"` 27 | FirstName Settable[string] `json:"firstName" yaml:"firstName"` 28 | Email Settable[string] `json:"email" yaml:"email"` 29 | Age Settable[int] `json:"age" yaml:"age"` 30 | Settings Settable[JSONB] `json:"settings" yaml:"settings"` 31 | } 32 | 33 | func (inp *User) ToSettable(inputFields map[string]interface{}) *UserSettable { 34 | settable := &UserSettable{} 35 | 36 | if _, ok := inputFields["id"]; ok { 37 | settable.ID = NewSettable(inp.ID) 38 | } 39 | 40 | if _, ok := inputFields["createdAt"]; ok { 41 | settable.CreatedAt = NewSettable(inp.CreatedAt) 42 | } 43 | 44 | if _, ok := inputFields["firstName"]; ok { 45 | settable.FirstName = NewSettable(inp.FirstName) 46 | } 47 | 48 | if _, ok := inputFields["email"]; ok { 49 | settable.Email = NewSettable(inp.Email) 50 | } 51 | 52 | if _, ok := inputFields["age"]; ok { 53 | settable.Age = NewSettable(inp.Age) 54 | } 55 | 56 | if _, ok := inputFields["settings"]; ok { 57 | settable.Settings = NewSettable(inp.Settings) 58 | } 59 | 60 | return settable 61 | } 62 | -------------------------------------------------------------------------------- /examples/my_project/responses/sort-by-keys_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/AlwxSin/simplegen, DO NOT EDIT. 2 | package responses 3 | 4 | import ( 5 | "examples/my_project/models" 6 | ) 7 | 8 | func UserListSortByKeys(vs []*models.User, keys []int) []*models.User { 9 | res := make([]*models.User, len(keys)) 10 | for i, key := range keys { 11 | var appendable *models.User 12 | for _, v := range vs { 13 | if key == v.ID { 14 | appendable = v 15 | break 16 | } 17 | } 18 | res[i] = appendable 19 | } 20 | return res 21 | } 22 | 23 | func UserListByEmailSortByKeys(vs []*models.User, keys []string) []*models.User { 24 | res := make([]*models.User, len(keys)) 25 | for i, key := range keys { 26 | var appendable *models.User 27 | for _, v := range vs { 28 | if key == v.Email { 29 | appendable = v 30 | break 31 | } 32 | } 33 | res[i] = appendable 34 | } 35 | return res 36 | } 37 | -------------------------------------------------------------------------------- /examples/my_project/responses/structs.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | import "examples/my_project/models" 4 | 5 | // simplegen:sort-by-keys -type *examples/my_project/models.User 6 | type UsersResponse struct { 7 | Users []*models.User 8 | RequestID string 9 | } 10 | 11 | // simplegen:sort-by-keys -type *examples/my_project/models.User -suffix ByEmail -fieldName Email -fieldType string 12 | type UsersResponseByEmail struct { 13 | Users []*models.User 14 | RequestID string 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AlwxSin/simplegen 2 | 3 | go 1.20 4 | 5 | require golang.org/x/tools v0.13.0 6 | 7 | require ( 8 | golang.org/x/mod v0.12.0 // indirect 9 | golang.org/x/sys v0.12.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 2 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 3 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 4 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 5 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 6 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 7 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 8 | -------------------------------------------------------------------------------- /simplegen.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package simplegen helps developer to generate code. 3 | 4 | For codegen more complex than just generate small piece of code 5 | usually need to parse source code and build ast.Tree. 6 | Simplegen will take this part and give developer exact ast.Node to work with. 7 | 8 | Also, simplegen provides tools for easy finding ast.Node. 9 | 10 | Example: 11 | 12 | package main 13 | 14 | import ( 15 | "flag" 16 | "fmt" 17 | "go/ast" 18 | "golang.org/x/tools/go/packages" 19 | 20 | "github.com/AlwxSin/simplegen" 21 | ) 22 | 23 | var PaginatorTemplate = ` 24 | {{ range $key, $struct := .Specs }} 25 | // {{$struct.Name}}ListPaginated represents {{$struct.Name}} list in a pagination container. 26 | type {{$struct.Name}}ListPaginated struct { 27 | CurrentCursor *string ` + "`json:\"currentCursor\"`\n" + 28 | ` NextCursor *string ` + "`json:\"nextCursor\"`\n" + 29 | ` Results []*{{$struct.Name}} ` + "`json:\"results\"`\n" + 30 | ` 31 | isPaginated bool 32 | limit int 33 | offset int 34 | } 35 | ` 36 | 37 | func Paginator( 38 | sg *simplegen.SimpleGenerator, 39 | pkg *packages.Package, 40 | node *ast.TypeSpec, 41 | comment *ast.Comment, 42 | ) (templateData simplegen.SpecData, imports []string, err error) { 43 | imports = append(imports, "strconv") 44 | 45 | type PaginatorTypeSpec struct { 46 | Name string 47 | } 48 | 49 | tmplData := &PaginatorTypeSpec{ 50 | Name: node.Name.Name, 51 | } 52 | return simplegen.SpecData(tmplData), imports, nil 53 | } 54 | 55 | // simplegen:paginator 56 | type User struct { 57 | Email string 58 | } 59 | 60 | func main() { 61 | var pn simplegen.PackageNames 62 | 63 | flag.Var(&pn, "package", "Package where simplegen should find magic comments") 64 | flag.Parse() 65 | 66 | sg, err := simplegen.NewSimpleGenerator(pn, simplegen.GeneratorsMap{ 67 | "paginator": simplegen.TemplateGenerator{ 68 | Template: PaginatorTemplate, 69 | GeneratorFunc: Paginator, 70 | }, 71 | }, nil) 72 | if err != nil { 73 | fmt.Println(err) 74 | return 75 | } 76 | 77 | err = sg.Generate() 78 | if err != nil { 79 | fmt.Println(err) 80 | } 81 | } 82 | 83 | In result simplegen will generate file with following content 84 | Example: 85 | 86 | // UserListPaginated represents User list in a pagination container. 87 | type UserListPaginated struct { 88 | CurrentCursor *string `json:"currentCursor"` 89 | NextCursor *string `json:"nextCursor"` 90 | Results []*User `json:"results"` 91 | 92 | isPaginated bool 93 | limit int 94 | offset int 95 | } 96 | 97 | See /examples dir for more detailed usage. 98 | */ 99 | package simplegen 100 | 101 | import ( 102 | "bytes" 103 | "fmt" 104 | "go/ast" 105 | "go/format" 106 | "go/token" 107 | "go/types" 108 | "os" 109 | "path/filepath" 110 | "strings" 111 | "text/template" 112 | 113 | "golang.org/x/tools/go/packages" 114 | ) 115 | 116 | const packagesLoadMode = packages.NeedName | 117 | packages.NeedTypes | 118 | packages.NeedSyntax | 119 | packages.NeedTypesInfo | 120 | packages.NeedImports | 121 | packages.NeedModule 122 | 123 | type SimpleGenerator struct { 124 | // pkgs collects all used packages for easy use 125 | pkgs map[pkgPath]*packages.Package 126 | 127 | generators GeneratorsMap 128 | cmdData map[GeneratorName]map[*packages.Package]*cmdData 129 | 130 | tmplFuncMap template.FuncMap 131 | } 132 | 133 | func NewSimpleGenerator(pkgNames PackageNames, generators GeneratorsMap, tmplFuncMap template.FuncMap) (*SimpleGenerator, error) { 134 | dir, err := os.Getwd() 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | fset := token.NewFileSet() 140 | cfg := &packages.Config{Fset: fset, Mode: packagesLoadMode, Dir: dir} 141 | pkgs, err := packages.Load(cfg, 142 | pkgNames..., 143 | ) 144 | if err != nil { 145 | return nil, fmt.Errorf("cannot load packages %s: %w", pkgNames, err) 146 | } 147 | 148 | errors := sgErrors{} 149 | 150 | sg := &SimpleGenerator{ 151 | generators: generators, 152 | pkgs: make(map[pkgPath]*packages.Package), 153 | cmdData: make(map[GeneratorName]map[*packages.Package]*cmdData), 154 | tmplFuncMap: tmplFuncMap, 155 | } 156 | for _, pkg := range pkgs { 157 | sg.pkgs[pkgPath(pkg.PkgPath)] = pkg 158 | } 159 | 160 | if len(errors) > 0 { 161 | return nil, errors 162 | } 163 | 164 | return sg, nil 165 | } 166 | 167 | func (sg *SimpleGenerator) Generate() error { 168 | errors := sgErrors{} 169 | 170 | // first, inspect ast of loaded packages to find 171 | for _, pkg := range sg.pkgs { 172 | for _, fileAst := range pkg.Syntax { 173 | ast.Inspect(fileAst, func(n ast.Node) bool { 174 | switch node := n.(type) { 175 | case *ast.GenDecl: 176 | copyGenDeclCommentsToSpecs(node) 177 | case *ast.TypeSpec: 178 | if node.Doc == nil { 179 | return true 180 | } 181 | for _, comment := range node.Doc.List { 182 | if strings.Contains(comment.Text, CmdKey) { 183 | for cmd, generator := range sg.generators { 184 | if strings.Contains(comment.Text, string(cmd)) { 185 | err := sg.add(cmd, pkg, node, comment, generator.GeneratorFunc) 186 | if err != nil { 187 | errors = append(errors, err) 188 | } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | return true 195 | }) 196 | } 197 | } 198 | if len(errors) > 0 { 199 | return errors 200 | } 201 | 202 | // second, write collected specs to files 203 | return sg.write() 204 | } 205 | 206 | func (sg *SimpleGenerator) add( 207 | genName GeneratorName, 208 | pkg *packages.Package, 209 | node *ast.TypeSpec, 210 | comment *ast.Comment, 211 | genFunc GeneratorFunc, 212 | ) error { 213 | if _, ok := sg.cmdData[genName]; !ok { 214 | sg.cmdData[genName] = make(map[*packages.Package]*cmdData) 215 | } 216 | 217 | templateData, rawImports, err := genFunc(sg, pkg, node, comment) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | importsMap := map[string]struct{}{} 223 | var imports []string 224 | for _, rawImp := range rawImports { 225 | if _, ok := importsMap[rawImp]; !ok { 226 | importsMap[rawImp] = struct{}{} 227 | imports = append(imports, rawImp) 228 | } 229 | } 230 | 231 | _, ok := sg.cmdData[genName][pkg] 232 | if !ok { 233 | sg.cmdData[genName][pkg] = newGeneratorData(pkg.Name, imports) 234 | } 235 | 236 | sg.cmdData[genName][pkg].add(templateData) 237 | return nil 238 | } 239 | 240 | func (sg *SimpleGenerator) write() error { 241 | errors := sgErrors{} 242 | 243 | for genName, genData := range sg.cmdData { 244 | cmdTemplate := sg.generators[genName].Template 245 | 246 | templateRaw := header + cmdTemplate 247 | tmpl := template.Must(template.New("").Funcs(sg.tmplFuncMap).Parse(templateRaw)) 248 | 249 | for pkg, specs := range genData { 250 | buf := bytes.Buffer{} 251 | 252 | if err := tmpl.Execute(&buf, specs); err != nil { 253 | return err 254 | } 255 | 256 | content, err := format.Source(buf.Bytes()) 257 | if err != nil { 258 | errors = append(errors, err) 259 | continue 260 | } 261 | 262 | pkgDir := filepath.Join(pkg.Module.Dir, strings.TrimPrefix(pkg.PkgPath, pkg.Module.Path)) 263 | 264 | fName := fmt.Sprintf("%s_gen.go", genName) 265 | 266 | err = writeFile(filepath.Join(pkgDir, fName), content) 267 | if err != nil { 268 | errors = append(errors, err) 269 | } 270 | } 271 | } 272 | if len(errors) > 0 { 273 | return errors 274 | } 275 | return nil 276 | } 277 | 278 | // GetPackage returns packages.Package. It tries to load package if it didn't load before. 279 | func (sg *SimpleGenerator) GetPackage(path string) (*packages.Package, error) { 280 | pkg, ok := sg.pkgs[pkgPath(path)] 281 | if !ok { 282 | pkgs, err := packages.Load(&packages.Config{Mode: packagesLoadMode}, path) 283 | if err != nil { 284 | return nil, err 285 | } 286 | if len(pkgs) != 1 { 287 | return nil, fmt.Errorf("too many packages found for path: %s", path) 288 | } 289 | pkg = pkgs[0] 290 | sg.pkgs[pkgPath(path)] = pkg 291 | } 292 | return pkg, nil 293 | } 294 | 295 | // GetObject tries to find type object in given package. 296 | // In most cases you don't need it, use GetStructType instead. 297 | func (sg *SimpleGenerator) GetObject(pkg *packages.Package, typeName string) (types.Object, error) { 298 | obj := pkg.Types.Scope().Lookup(typeName) 299 | if obj == nil { 300 | return nil, fmt.Errorf("%s not found in declared types of %s", 301 | typeName, pkg) 302 | } 303 | 304 | // check if it is a declared type 305 | if _, ok := obj.(*types.TypeName); !ok { 306 | return nil, fmt.Errorf("%v is not a named type", obj) 307 | } 308 | return obj, nil 309 | } 310 | 311 | // GetStructType tries to find type struct in given package. 312 | func (sg *SimpleGenerator) GetStructType(pkg *packages.Package, typeName string) (*types.Struct, error) { 313 | obj, err := sg.GetObject(pkg, typeName) 314 | if err != nil { 315 | return nil, err 316 | } 317 | 318 | // expect the underlying type to be a struct 319 | structType, ok := obj.Type().Underlying().(*types.Struct) 320 | if !ok { 321 | return nil, fmt.Errorf("type %v is not a struct", obj) 322 | } 323 | 324 | return structType, nil 325 | } 326 | 327 | // writeFile (re)creates a new file and writes content into it. 328 | func writeFile(fileName string, fileContent []byte) error { 329 | f, err := os.Create(fileName) 330 | if err != nil { 331 | return err 332 | } 333 | defer f.Close() 334 | 335 | _, err = f.Write(fileContent) 336 | return err 337 | } 338 | 339 | // copyDocsToSpecs will take the GenDecl level documents and copy them 340 | // to the children Type and Value specs. I think this is actually working 341 | // around a bug in the AST, but it works for now. 342 | func copyGenDeclCommentsToSpecs(x *ast.GenDecl) { 343 | // Copy the doc spec to the type or value spec 344 | // cause they missed this... whoops 345 | if x.Doc != nil { 346 | for _, spec := range x.Specs { 347 | if s, ok := spec.(*ast.TypeSpec); ok { 348 | if s.Doc == nil { 349 | s.Doc = x.Doc 350 | } 351 | } 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package simplegen 2 | 3 | const header = `// Code generated by github.com/AlwxSin/simplegen, DO NOT EDIT. 4 | package {{.PackageName}} 5 | 6 | {{ if ne (len .Imports) 0 }} 7 | import ( 8 | {{- range $index, $value := .Imports }} 9 | "{{$value}}" 10 | {{- end -}} 11 | ) 12 | {{end}} 13 | 14 | ` 15 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package simplegen 2 | 3 | import ( 4 | "errors" 5 | "go/ast" 6 | "strings" 7 | 8 | "golang.org/x/tools/go/packages" 9 | ) 10 | 11 | const CmdKey = "simplegen" 12 | 13 | type sgErrors []error 14 | 15 | func (e sgErrors) Error() string { 16 | return errors.Join(e...).Error() 17 | } 18 | 19 | type pkgPath string 20 | 21 | type GeneratorName string 22 | 23 | // GeneratorFunc for generating template data from ast nodes. 24 | // SimpleGenerator calls it with: 25 | // sg -> SimpleGenerator instance (for useful methods, look into docs or examples to know how to use them). 26 | // pkg -> packages.Package where magic comment was found. 27 | // node -> ast.TypeSpec struct annotated with magic comment. 28 | // comment -> ast.Comment magic comment itself. 29 | type GeneratorFunc func( 30 | sg *SimpleGenerator, 31 | pkg *packages.Package, 32 | node *ast.TypeSpec, 33 | comment *ast.Comment, 34 | ) (templateData SpecData, imports []string, err error) 35 | 36 | // TemplateGenerator contains raw template and GeneratorFunc to generate template data. 37 | type TemplateGenerator struct { 38 | // Template is a string which contains full template in go style 39 | Template string 40 | GeneratorFunc GeneratorFunc 41 | } 42 | 43 | // GeneratorsMap cmd_name -> func_to_generate_template_data 44 | // 45 | // { 46 | // "paginator": GeneratePaginatorData, 47 | // "sorter": GenerateSorterData, 48 | // } 49 | type GeneratorsMap map[GeneratorName]TemplateGenerator 50 | 51 | // SpecData can be any struct. Will pass it to template. 52 | type SpecData any 53 | 54 | // cmdData internal struct. Will pass it to template. 55 | type cmdData struct { 56 | PackageName string 57 | Imports []string 58 | 59 | Specs []SpecData 60 | } 61 | 62 | func newGeneratorData(pkgName string, imports []string) *cmdData { 63 | return &cmdData{ 64 | PackageName: pkgName, 65 | Imports: imports, 66 | Specs: make([]SpecData, 0), 67 | } 68 | } 69 | 70 | func (gd *cmdData) add(sd SpecData) { 71 | gd.Specs = append(gd.Specs, sd) 72 | } 73 | 74 | // PackageNames is a helper for flag.Parse 75 | // Example: 76 | // flag.Var(&pn, "package", "Package where simplegen should find magic comments"). 77 | type PackageNames []string 78 | 79 | func (pn PackageNames) String() string { 80 | return strings.Join(pn, ",") 81 | } 82 | 83 | func (pn *PackageNames) Set(value string) error { 84 | *pn = append(*pn, value) 85 | return nil 86 | } 87 | --------------------------------------------------------------------------------