├── .github ├── ISSUE_TEMPLATE │ └── issue.md ├── logo.svg └── workflows │ └── cicd.yml ├── .gitignore ├── .golangci.yml ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE.md ├── README.md ├── cli ├── cli.go ├── config │ ├── models.go │ └── yml.go ├── generator │ ├── generator.go │ ├── interpreter │ │ ├── extract │ │ │ ├── extract.go │ │ │ └── github_com-switchupcb-copygen-cli-models.go │ │ └── interpreter.go │ └── template │ │ ├── LICENSE.md │ │ └── generate.go ├── matcher │ ├── matcher.go │ └── relation.go ├── models │ ├── category.go │ ├── debug │ │ └── debug.go │ ├── field.go │ ├── function.go │ ├── generator.go │ └── type.go └── parser │ ├── ast.go │ ├── field.go │ ├── function.go │ ├── keep.go │ ├── options │ ├── cast.go │ ├── convert.go │ ├── custom.go │ ├── deepcopy.go │ ├── depth.go │ ├── matcher.go │ └── options.go │ ├── parse.go │ └── type.go ├── examples ├── LICENSE.md ├── _tests │ ├── README.md │ ├── alias │ │ ├── copygen.go │ │ └── setup │ │ │ ├── setup.go │ │ │ └── setup.yml │ ├── automap │ │ ├── copygen.go │ │ └── setup │ │ │ ├── setup.go │ │ │ └── setup.yml │ ├── cyclic │ │ ├── copygen.go │ │ ├── domain │ │ │ └── domain.go │ │ ├── duplicate │ │ │ └── duplicate.go │ │ ├── models │ │ │ └── model.go │ │ └── setup │ │ │ ├── setup.go │ │ │ └── setup.yml │ ├── duplicate │ │ ├── copygen.go │ │ ├── domain │ │ │ └── domain.go │ │ ├── models │ │ │ └── models.go │ │ └── setup │ │ │ ├── setup.go │ │ │ └── setup.yml │ ├── examples_test.go │ ├── file_test.go │ ├── import │ │ ├── copygen.go │ │ ├── domain.go │ │ ├── models │ │ │ └── model.go │ │ └── setup │ │ │ ├── setup.go │ │ │ └── setup.yml │ ├── multi │ │ ├── complex │ │ │ └── complex.go │ │ ├── copygen.go │ │ ├── external │ │ │ └── external.go │ │ └── setup │ │ │ ├── setup.go │ │ │ └── setup.yml │ ├── option │ │ └── setup │ │ │ ├── setup.go │ │ │ └── setup.yml │ ├── option_test.go │ └── same │ │ └── setup │ │ ├── copygen.go │ │ ├── setup.go │ │ └── setup.yml ├── automatch │ ├── README.md │ ├── copygen.go │ ├── domain │ │ └── domain.go │ ├── models │ │ └── model.go │ └── setup │ │ ├── setup.go │ │ └── setup.yml ├── basic │ ├── README.md │ ├── copygen.go │ ├── domain │ │ └── domain.go │ ├── models │ │ └── model.go │ └── setup │ │ ├── setup.go │ │ └── setup.yml ├── cast │ ├── README.md │ ├── assert │ │ ├── assert.go │ │ ├── setup.go │ │ └── setup.yml │ ├── convert │ │ ├── convert.go │ │ ├── copygen.go │ │ ├── setup.go │ │ └── setup.yml │ ├── depth │ │ ├── depth.go │ │ ├── setup.go │ │ └── setup.yml │ ├── expression │ │ ├── setup.go │ │ └── setup.yml │ ├── function │ │ ├── function.go │ │ ├── setup.go │ │ └── setup.yml │ └── property │ │ ├── property.go │ │ ├── setup.go │ │ └── setup.yml ├── deepcopy │ └── .keep ├── error │ ├── README.md │ ├── copygen.go │ ├── domain │ │ └── domain.go │ ├── models │ │ └── model.go │ ├── setup │ │ ├── setup.go │ │ └── setup.yml │ └── template │ │ └── generate.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── main │ ├── README.md │ ├── copygen.go │ ├── domain │ │ └── domain.go │ ├── models │ │ └── model.go │ └── setup │ │ ├── setup.go │ │ └── setup.yml ├── map │ ├── README.md │ ├── copygen.go │ ├── domain │ │ └── domain.go │ ├── models │ │ └── model.go │ └── setup │ │ ├── setup.go │ │ └── setup.yml ├── program │ ├── README.md │ └── main.go ├── tag │ ├── README.md │ ├── copygen.go │ ├── domain │ │ └── domain.go │ ├── models │ │ └── model.go │ └── setup │ │ ├── setup.go │ │ └── setup.yml └── tmpl │ ├── README.md │ ├── copygen.go │ ├── domain │ └── domain.go │ ├── models │ └── model.go │ ├── setup │ ├── setup.go │ └── setup.yml │ └── template │ └── generate.tmpl ├── go.mod ├── go.sum └── main.go /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ISSUE 3 | about: Create a report for an unexpected error. 4 | title: '' 5 | labels: edge case 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please provide the following information. 11 | 12 | ## Setup 13 | 14 | ### YML 15 | 16 | ```yml 17 | paste your .yml setup file here. 18 | ``` 19 | 20 | ### Go 21 | 22 | ```go 23 | paste your .go setup file here. 24 | ``` 25 | 26 | ## Output 27 | 28 | ### Error 29 | 30 | ``` 31 | paste any error messages here. 32 | ``` 33 | 34 | ### Generation 35 | 36 | ```go 37 | paste any generated code here. 38 | ``` 39 | 40 | ## Environment 41 | 42 | **Operating System**: (i.e ubuntu, windows, iOS, etc) 43 | **Copygen Version**: (i.e latest, v0.2, etc) 44 | -------------------------------------------------------------------------------- /.github/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: "CICD" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | Linter: 13 | name: Static Code Analysis 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository code 17 | uses: actions/checkout@v3 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: '1.23' 22 | - name: Perform static code analysis via golangci-lint 23 | uses: golangci/golangci-lint-action@v3 24 | with: 25 | version: v1.64.5 26 | 27 | Integration: 28 | needs: Linter 29 | name: Integration Tests 30 | runs-on: '${{ matrix.os }}' 31 | strategy: 32 | matrix: 33 | os: 34 | - windows-latest 35 | - macos-latest 36 | - ubuntu-latest 37 | steps: 38 | - name: Checkout repository code 39 | uses: actions/checkout@v3 40 | - name: Set up Go 41 | uses: actions/setup-go@v4 42 | with: 43 | go-version: '1.23' 44 | - name: Run tests 45 | working-directory: ./examples 46 | run: go test ./_tests 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | tests: false 4 | 5 | linters: 6 | enable-all: true 7 | disable: 8 | - gomoddirectives # The repository uses go modules in its interpreter functionality from a temporary tagged fork. 9 | 10 | - cyclop 11 | - depguard 12 | - err113 # future implementation 13 | - exhaustruct 14 | - forbidigo 15 | - funlen # can't handle switch statement 16 | - gochecknoglobals # No data race conditions 17 | - gocognit 18 | - gofumpt # too many false positives 19 | - intrange 20 | - lll 21 | - mnd 22 | - nestif # can't handle cache 23 | - nlreturn 24 | - unparam # generator function 25 | - varnamelen # for loops 26 | - whitespace 27 | - wsl 28 | 29 | - tenv # deprecated 30 | 31 | 32 | fast: false 33 | linters-settings: 34 | govet: 35 | enable-all: true 36 | disable: 37 | - shadow 38 | # - fieldalignment 39 | wrapcheck: 40 | ignorePackageGlobs: 41 | - github.com/switchupcb/copygen/cli/generator/* 42 | gocritic: 43 | settings: 44 | ifElseChain: 45 | minThreshold: 3 46 | revive: 47 | rules: 48 | - name: unused-parameter 49 | disabled: true 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Contributor License Agreement 4 | 5 | Contributions to this project must be accompanied by a **Contributor License Agreement**. 6 | 7 | You or your employer retain the copyright to your contribution: Accepting this agreement gives us permission to use and redistribute your contributions as part of the project. 8 | 9 | ## Pull Requests 10 | 11 | Pull requests must pass all [CI/CD](#cicd) measures and follow the [code specification](#specification). 12 | 13 | ## Domain 14 | 15 | The domain of Copygen lies in field manipulation. The program uses provided types to determine the fields we must assign. In the context of this domain, a `Type` refers to _the types used in a function (as parameters or results) [e.g., `func example(x TypeX) y TypeY`]_, and not a type used to define variables (e.g., `type example string`). 16 | 17 | ## Project Structure 18 | 19 | The repository consists of a detailed [README](README.md), [examples](/examples/), and [**command line interface**](/cli/). 20 | 21 | ### Command Line Interface 22 | 23 | The command-line interface _(cli)_ consists of 5 packages. 24 | 25 | | Package | Description | 26 | | :-------- | :------------------------------------------------------------------------------------------------- | 27 | | cli | Contains the primary logic used to parse arguments and run the `copygen` command-line application. | 28 | | models | Contains models based on the application's functionality _(business logic)_. | 29 | | config | Contains external loaders used to configure the file settings and command line options. | 30 | | parser | Uses an Abstract Syntax Tree (AST) and `go/types` to parse a setup file for fields. | 31 | | matcher | Contains application logic to match fields to each other. | 32 | | generator | Contains the generator logic used to generate code _(and interpret templates)_. | 33 | 34 | _Read [program](examples/program/README.md) for an overview of the application's code._ 35 | 36 | 37 | ### Parser 38 | 39 | A `setup` file's abstract syntax tree is traversed once, but involves four processes. 40 | 41 | #### 1. Keep 42 | 43 | The `setup` file is parsed using an Abstract Syntax Tree. 44 | 45 | This tree contains the `type Copygen Interface` but also code that must be **kept** in the generated `output` file. 46 | 47 | For example, the package declaration, file imports, convert functions, and [custom types](README.md#custom-types) all exist _outside_ of the `type Copygen Interface`. Instead of storing these declarations and attempting to regenerate them, we simply discard declarations — from the `setup` file's AST — that won't be kept: In this case, the `type Copygen Interface` and `ast.Comments` (that refer to `Options`). 48 | 49 | #### 2. Options 50 | 51 | **Convert** options are defined **outside** of the `type Copygen Interface` and may apply to multiple functions. So, all `ast.Comments` must be parsed before `models.Function` and `models.Field` objects can be created. In order to do this, the `type Copygen Interface` is stored, but **NOT** analyzed until the `setup` file is traversed. 52 | 53 | There are multiple ways to parse `ast.Comments` into `Options`, but **convert** options require the name of their respective **convert** functions _(which can't be parsed from comments)_. So, the most readable, efficient, and least error prone method of parsing `ast.Comments` into `Options` is to parse them when discovered and assign them from a `CommentOptionMap` later. In addition, regex compilation is expensive — [especially in Go](https://github.com/mariomka/regex-benchmark#performance) — and avoided by only compiling unique comments once. 54 | 55 | #### 3. Copygen Interface 56 | 57 | The `type Copygen interface` is parsed to setup the `models.Function` and `models.Field` objects used in the `Matcher` and `Generator`. 58 | - [go/types Contents (Types, A -> B)](https://go.googlesource.com/example/+/HEAD/gotypes#contents) 59 | - [go/packages Package Object](https://pkg.go.dev/golang.org/x/tools/go/packages#Package) 60 | - [go/types Func (Signature)](https://pkg.go.dev/go/types#Func) 61 | - [go/types Types](https://pkg.go.dev/go/types#pkg-types) 62 | 63 | #### 4. Imports 64 | 65 | The `go/types` package provides all of the other important information _**except**_ for alias import names. In order to assign aliased or non-aliased import names to `models.Field`, the imports of the `setup` file are mapped to a package path, then assigned to fields prior to matching. 66 | 67 | ### Generator 68 | 69 | Copygen supports three methods of generation for end-users _(developers)_: `.go`, `.tmpl`, and `programmatic`. 70 | 71 | #### .go 72 | 73 | `.go` code generation allows users to generate code using the programming language they are familiar with. 74 | 75 | `.go` code generation works by allowing the end-user to specify **where** _the `.go` file containing the code generation algorithm_ is, then running the file _at runtime_. We must use an **interpreter** to provide this functionality. 76 | 77 | `.go` templates are interpreted by a [yaegi fork](https://github.com/switchupcb/yaegi). 78 | 1. `models` objects are extracted via reflection and loaded into the interpreter. 79 | 2. Then, the interpreter interprets the provided `.go` template file _(specified by the user)_ to run the `Generate()` function. 80 | 81 | #### .tmpl 82 | 83 | `.tmpl` code generation allows users to generate code using [`text/templates`](https://pkg.go.dev/text/template). 84 | 85 | `.tmpl` code generation works by allowing the end-user to specify **where** _the `.tmpl` file containing the code generation algorithm_ is, then parsing and executing the file _at runtime_. 86 | 87 | #### programmatic 88 | 89 | `programmatic` code generation lets users generate code by using `copygen` as a third-party module. For more information, read the [program example](/examples/program/README.md). 90 | 91 | ## Specification 92 | 93 | ### From vs. To 94 | 95 | From and To is used to denote the direction of a type or field. A from-field is assigned **to** a to-field. In contrast, one from-field can match many to-fields. So, **"From" comes before "To" when parsing** while **"To" comes before "From" when matching**. 96 | 97 | ### Variable Names 98 | 99 | | Variable | Description | 100 | | :------- | :------------------------------------------------------ | 101 | | from.* | Variables preceded by from indicate from-functionality. | 102 | | to.* | Variables preceded by to indicate to-functionality. | 103 | 104 | ### Comments 105 | 106 | Comments follow [Effective Go](https://golang.org/doc/effective_go#commentary) and explain why more than what _(unless the "what" isn't intuitive)_. 107 | 108 | ### Why Pointers 109 | 110 | Contrary to the README, pointers aren't used — on `models.Fields` — as a performance optimization. Using pointers with `models.Fields` makes it less likely for a mistake to occur during their comparison. For example, using a for-copy loop on a `[]models.Field`: 111 | 112 | ```go 113 | // A copy of field is created with a distinct memory address. 114 | for _, field := range fields { 115 | // field.To still points to the original field's .To memory address. 116 | // field.To.From points to the original field's memory address, which is NOT the copied field's memory address, even though both fields' fields have the same values. 117 | if field == field.To.From { 118 | // never happens 119 | ... 120 | } 121 | } 122 | ``` 123 | 124 | ### Anti-patterns 125 | 126 | Using the `*models.Field` definition for a `models.Field`'s `Parent` field can be considered an anti-pattern. In the program, a `models.Type` specifically refers to the types in a function signature _(i.e `func(models.Account, models.User) *domain.Account`)_. While these types **are** fields _(which may contain other fields)_ , their actual `Type` properties are not relevant to `models.Field`. So, `models.Field` objects are pointed directly to each other for simplicity. 127 | 128 | Using the `*models.Field` definition for a `models.Field`'s `From` and `To` fields can be placed into a `type FieldRelation` since `From` and `To` is only assigned in the matcher. While either method allows you to reference a `models.Field`'s respective `models.Field`, directly pointing `models.Field` objects adds more customizability to the program for the end user. 129 | 130 | ## CI/CD 131 | 132 | ### Static Code Analysis 133 | 134 | Copygen uses [golangci-lint](https://github.com/golangci/golangci-lint) in order to statically analyze code. You can install golangci-lint with `go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5` and run it using `golangci-lint run`. If you receive a `diff` error, you must add a `diff` tool in your PATH. There is one located in the `Git` bin. 135 | 136 | If you receive `File is not ... with -...`, use `golangci-lint run --disable-all --no-config -Egofmt --fix`. 137 | 138 | #### Fieldalignment 139 | 140 | **Struct padding** aligns the fields of a struct to addresses in memory. The compiler does this to improve performance and prevent numerous issues on a system's architecture _(32-bit, 64-bit)_. So, misaligned fields add more memory-usage to a program, which can effect performance in a numerous amount of ways. For a simple explanation, view [Golang Struct Size and Memory Optimization](https://medium.com/techverito/golang-struct-size-and-memory-optimisation-b46b124f008d 141 | ). 142 | 143 | Fieldalignment can be fixed using the [fieldalignment tool](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment) which is installed using `go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest`. 144 | 145 | **ALWAYS COMMIT BEFORE USING `fieldalignment -fix ./cli/...`** as it may remove comments. 146 | 147 | ### Tests 148 | 149 | For information on testing, read [Tests](examples/_tests/). 150 | 151 | # Roadmap 152 | 153 | Implement the following features. 154 | - Generator: deepcopy 155 | - Parser: Fix Free-floating comments _(add structs in [`multi`](examples/_tests/multi/copygen.go) to test)_ 156 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2022 SwitchUpCB 2 | 3 | This file is part of Copygen. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/switchupcb/copygen/cli/config" 11 | "github.com/switchupcb/copygen/cli/generator" 12 | "github.com/switchupcb/copygen/cli/matcher" 13 | "github.com/switchupcb/copygen/cli/parser" 14 | ) 15 | 16 | // Environment represents the copygen environment. 17 | type Environment struct { 18 | YMLPath string // The .yml file path used as a configuration file. 19 | Output bool // Whether to print the generated code to stdout. 20 | Write bool // Whether to write the generated code to a file. 21 | } 22 | 23 | // CLI runs copygen from a Command Line Interface and returns the exit status. 24 | func CLI() int { 25 | var env Environment 26 | 27 | if err := env.parseArgs(); err != nil { 28 | fmt.Fprintf(os.Stderr, "%v\n", err) 29 | return 2 30 | } 31 | 32 | if _, err := env.Run(); err != nil { 33 | fmt.Fprintf(os.Stderr, "%v\n", err) 34 | return 1 35 | } 36 | 37 | return 0 38 | } 39 | 40 | // parseArgs parses the provided command line arguments. 41 | func (e *Environment) parseArgs() error { 42 | // define the command line arguments. 43 | var ( 44 | ymlpath = flag.String("yml", "", "The path to the .yml flag used for code generation (from the current working directory).") 45 | output = flag.Bool("o", false, "Use -o to print generated code to the screen.") 46 | ) 47 | 48 | // parse the command line arguments. 49 | flag.Parse() 50 | 51 | if !strings.HasSuffix(*ymlpath, ".yml") { 52 | return errors.New("you must specify a .yml configuration file using -yml") 53 | } 54 | 55 | e.YMLPath = *ymlpath 56 | e.Output = *output 57 | e.Write = true 58 | 59 | return nil 60 | } 61 | 62 | // Run runs copygen programmatically using the given Environment's YMLPath. 63 | func (e *Environment) Run() (string, error) { 64 | // The configuration file is loaded (.yml) 65 | gen, err := config.LoadYML(e.YMLPath) 66 | if err != nil { 67 | return "", fmt.Errorf("%w", err) 68 | } 69 | 70 | // The data file is parsed (.go) 71 | if err = parser.Parse(gen); err != nil { 72 | return "", fmt.Errorf("%w", err) 73 | } 74 | 75 | // The matcher is run on the parsed data (to create the objects used during generation). 76 | if !gen.Options.Matcher.Skip { 77 | if err = matcher.Match(gen); err != nil { 78 | return "", fmt.Errorf("%w", err) 79 | } 80 | } 81 | 82 | // The generator is used to generate code. 83 | code, err := generator.Generate(gen, e.Output, e.Write) 84 | if err != nil { 85 | return "", fmt.Errorf("%w", err) 86 | } 87 | 88 | return code, nil 89 | } 90 | -------------------------------------------------------------------------------- /cli/config/models.go: -------------------------------------------------------------------------------- 1 | // Package config loads configuration data from an external file. 2 | package config 3 | 4 | // YML represents the first level of the YML file. 5 | type YML struct { 6 | Options map[string]interface{} `yaml:"custom"` 7 | Generated Generated `yaml:"generated"` 8 | Matcher Matcher `yaml:"matcher"` 9 | } 10 | 11 | // Generated represents generated properties of the YML file. 12 | type Generated struct { 13 | Setup string `yaml:"setup"` 14 | Output string `yaml:"output"` 15 | Template string `yaml:"template"` 16 | } 17 | 18 | // Matcher represents matcher properties of the YML file. 19 | type Matcher struct { 20 | Skip bool `yaml:"skip"` 21 | Cast Cast `yaml:"cast"` 22 | } 23 | 24 | // Cast represents matcher cast properties of the YML file. 25 | type Cast struct { 26 | Depth int `yaml:"depth"` 27 | Enabled bool `yaml:"enabled"` 28 | Disabled Disabled `yaml:"disabled"` 29 | } 30 | 31 | // Disabled represents matcher cast feature flags of the YML file. 32 | type Disabled struct { 33 | AssignObjectInterface bool `yaml:"assignObjectInterface"` 34 | AssertInterfaceObject bool `yaml:"assertInterfaceObject"` 35 | Convert bool `yaml:"convert"` 36 | } 37 | -------------------------------------------------------------------------------- /cli/config/yml.go: -------------------------------------------------------------------------------- 1 | // Package config loads configuration data from an external file. 2 | package config 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/switchupcb/copygen/cli/models" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // LoadYML loads a .yml configuration file into a Generator. 14 | func LoadYML(relativepath string) (*models.Generator, error) { 15 | file, err := os.ReadFile(relativepath) 16 | if err != nil { 17 | return nil, fmt.Errorf("the specified .yml filepath doesn't exist: %v\n%w", relativepath, err) 18 | } 19 | 20 | var yml YML 21 | if err := yaml.Unmarshal(file, &yml); err != nil { 22 | return nil, fmt.Errorf("an error occurred unmarshalling the .yml file\n%w", err) 23 | } 24 | 25 | gen := ParseYML(yml) 26 | 27 | // determine the actual filepath of the loader. 28 | absloadpath, err := filepath.Abs(relativepath) 29 | if err != nil { 30 | return nil, fmt.Errorf("an error occurred while determining the absolute file path of the loader file\n%v", relativepath) 31 | } 32 | 33 | // determine the actual filepath of the setup.go file. 34 | gen.Setpath = filepath.Join(filepath.Dir(absloadpath), gen.Setpath) 35 | 36 | // determine the actual filepath of the template file (if provided). 37 | if gen.Tempath != "" { 38 | gen.Tempath = filepath.Join(filepath.Dir(absloadpath), gen.Tempath) 39 | } 40 | 41 | // determine the actual filepath of the output file. 42 | gen.Outpath = filepath.Join(filepath.Dir(absloadpath), gen.Outpath) 43 | 44 | return gen, nil 45 | } 46 | 47 | // ParseYML parses a YML into a Generator. 48 | func ParseYML(yml YML) *models.Generator { 49 | return &models.Generator{ 50 | Setpath: yml.Generated.Setup, 51 | Outpath: yml.Generated.Output, 52 | Tempath: yml.Generated.Template, 53 | Options: models.GeneratorOptions{ 54 | Matcher: models.MatcherOptions{ 55 | Skip: yml.Matcher.Skip, 56 | AutoCast: yml.Matcher.Cast.Enabled, 57 | CastDepth: yml.Matcher.Cast.Depth, 58 | DisableAssignObjectInterface: yml.Matcher.Cast.Disabled.AssignObjectInterface, 59 | DisableAssertInterfaceObject: yml.Matcher.Cast.Disabled.AssertInterfaceObject, 60 | DisableConvert: yml.Matcher.Cast.Disabled.Convert, 61 | }, 62 | Custom: yml.Options, 63 | }, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cli/generator/generator.go: -------------------------------------------------------------------------------- 1 | // Package generator generates code. 2 | package generator 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "go/format" 9 | "os" 10 | "path/filepath" 11 | tmpl "text/template" 12 | 13 | "github.com/switchupcb/copygen/cli/generator/interpreter" 14 | "github.com/switchupcb/copygen/cli/generator/template" 15 | "github.com/switchupcb/copygen/cli/models" 16 | "golang.org/x/tools/imports" 17 | ) 18 | 19 | const ( 20 | GenerateFunction = "template.Generate" 21 | writeFileMode = 0644 22 | ) 23 | 24 | // Generate outputs the generated code (with gofmt). 25 | func Generate(gen *models.Generator, output bool, write bool) (string, error) { 26 | content, err := generate(gen) 27 | if err != nil { 28 | return "", fmt.Errorf("an error occurred while generating code.\n%w", err) 29 | } 30 | 31 | data := []byte(content) 32 | 33 | // imports 34 | importsdata, err := imports.Process(gen.Outpath, data, nil) 35 | if err != nil { 36 | if output { 37 | fmt.Println(content) 38 | return content, fmt.Errorf("an error occurred while formatting the generated code.\n%w", err) 39 | } 40 | 41 | return content, fmt.Errorf("an error occurred while formatting the generated code.\n%w\nUse -o to view output", err) 42 | } 43 | 44 | // gofmt 45 | fmtdata, err := format.Source(importsdata) 46 | if err != nil { 47 | if output { 48 | fmt.Println(string(importsdata)) 49 | return content, fmt.Errorf("an error occurred while formatting the generated code.\n%w", err) 50 | } 51 | 52 | return content, fmt.Errorf("an error occurred while formatting the generated code.\n%w\nUse -o to view output", err) 53 | } 54 | 55 | code := string(fmtdata) 56 | if output { 57 | fmt.Println(code) 58 | return code, nil 59 | } 60 | 61 | if write { 62 | if err := os.WriteFile(gen.Outpath, fmtdata, writeFileMode); err != nil { 63 | return code, fmt.Errorf("an error occurred creating the file.\n%w", err) 64 | } 65 | } 66 | 67 | return code, nil 68 | } 69 | 70 | // generate determines the method of code generation to use, 71 | // then generates the code. 72 | func generate(gen *models.Generator) (string, error) { 73 | if gen.Tempath != "" { 74 | ext := filepath.Ext(gen.Tempath) 75 | 76 | // generate code using a .go template. 77 | if ext == ".go" { 78 | return GenerateCode(gen) 79 | } 80 | 81 | // generate code using a .tmpl template. 82 | if ext == ".tmpl" { 83 | return GenerateTemplate(gen) 84 | } 85 | 86 | return "", fmt.Errorf("the provided template is not a `.go` or `.tmpl` file: %v", gen.Tempath) 87 | } 88 | 89 | // generate code using the default template. 90 | return template.Generate(gen) 91 | } 92 | 93 | // GenerateCode generates code using the default .go template. 94 | func GenerateCode(gen *models.Generator) (string, error) { 95 | // use an interpreted function (from a template file). 96 | v, err := interpreter.InterpretFunction(gen.Tempath, GenerateFunction) 97 | if err != nil { 98 | return "", fmt.Errorf("%w", err) 99 | } 100 | 101 | fn, ok := v.Interface().(func(*models.Generator) (string, error)) 102 | if !ok { 103 | return "", errors.New("the template function `Generate` could not be type asserted. Is it a func(*models.Generator) (string, error)?") 104 | } 105 | 106 | content, err := fn(gen) 107 | if err != nil { 108 | return "", fmt.Errorf("%w", err) 109 | } 110 | 111 | return content, nil 112 | } 113 | 114 | // GenerateTemplate generates code using a text/template file (.tmpl). 115 | func GenerateTemplate(gen *models.Generator) (string, error) { 116 | file, err := os.ReadFile(gen.Tempath) 117 | if err != nil { 118 | return "", fmt.Errorf("the specified .tmpl filepath doesn't exist: %v\n%w", gen.Tempath, err) 119 | } 120 | 121 | funcMap := tmpl.FuncMap{ 122 | "bytesToString": func(b []byte) string { return string(b) }, 123 | } 124 | 125 | t, err := tmpl.New("").Funcs(funcMap).Parse(string(file)) 126 | if err != nil { 127 | return "", fmt.Errorf("an error occurred parsing the .tmpl template file: %w", err) 128 | } 129 | 130 | buf := bytes.NewBuffer(nil) 131 | if err = t.Execute(buf, gen); err != nil { 132 | return "", fmt.Errorf("an error occurred executing the .tmpl template file: %w", err) 133 | } 134 | 135 | return buf.String(), nil 136 | } 137 | -------------------------------------------------------------------------------- /cli/generator/interpreter/extract/extract.go: -------------------------------------------------------------------------------- 1 | // Package extract uses the `yaegi extract` tool in order to generate the reflect.Value symbols of internal types. 2 | package extract 3 | 4 | import "reflect" 5 | 6 | // Symbols are extracted from the internal types (compiled at runtime). 7 | var Symbols = make(map[string]map[string]reflect.Value) 8 | 9 | //go:generate yaegi extract github.com/switchupcb/copygen/cli/models 10 | -------------------------------------------------------------------------------- /cli/generator/interpreter/extract/github_com-switchupcb-copygen-cli-models.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/switchupcb/copygen/cli/models'. DO NOT EDIT. 2 | 3 | package extract 4 | 5 | import ( 6 | "reflect" 7 | 8 | "github.com/switchupcb/copygen/cli/models" 9 | "github.com/switchupcb/copygen/cli/models/debug" 10 | 11 | ) 12 | 13 | func init() { 14 | Symbols["github.com/switchupcb/copygen/cli/models/models"] = map[string]reflect.Value{ 15 | // type definitions 16 | "Field": reflect.ValueOf((*models.Field)(nil)), 17 | "FieldOptions": reflect.ValueOf((*models.FieldOptions)(nil)), 18 | "Function": reflect.ValueOf((*models.Function)(nil)), 19 | "FunctionOptions": reflect.ValueOf((*models.FunctionOptions)(nil)), 20 | "Generator": reflect.ValueOf((*models.Generator)(nil)), 21 | "GeneratorOptions": reflect.ValueOf((*models.GeneratorOptions)(nil)), 22 | "Type": reflect.ValueOf((*models.Type)(nil)), 23 | } 24 | 25 | Symbols["github.com/switchupcb/copygen/cli/models/models/debug"] = map[string]reflect.Value{ 26 | // function, constant and variable definitions 27 | "PrintFieldGraph": reflect.ValueOf(debug.PrintFieldGraph), 28 | "PrintFieldRelation": reflect.ValueOf(debug.PrintFieldRelation), 29 | "PrintFieldTree": reflect.ValueOf(debug.PrintFieldTree), 30 | "PrintFunctionFields": reflect.ValueOf(debug.PrintFunctionFields), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cli/generator/interpreter/interpreter.go: -------------------------------------------------------------------------------- 1 | // Package interpreter interprets template code at runtime. 2 | package interpreter 3 | 4 | import ( 5 | "fmt" 6 | "go/build" 7 | "os" 8 | "reflect" 9 | 10 | "github.com/switchupcb/copygen/cli/generator/interpreter/extract" 11 | "github.com/switchupcb/yaegi/interp" 12 | "github.com/switchupcb/yaegi/stdlib" 13 | ) 14 | 15 | // InterpretFunction loads a template symbol from an interpreter. 16 | func InterpretFunction(filepath, symbol string) (*reflect.Value, error) { 17 | file, err := os.ReadFile(filepath) 18 | if err != nil { 19 | return nil, fmt.Errorf("an error occurred loading a template file: %v\nIs the relative or absolute filepath set correctly?\n%w", filepath, err) 20 | } 21 | 22 | // setup the interpreter 23 | goCache, err := os.UserCacheDir() 24 | if err != nil { 25 | return nil, fmt.Errorf("an error occurred loading the template file. Is the GOCACHE set in `go env`?\n%w", err) 26 | } 27 | 28 | i := interp.New(interp.Options{GoPath: os.Getenv("GOPATH"), GoCache: goCache, GoToolDir: build.ToolDir}) 29 | if err := i.Use(stdlib.Symbols); err != nil { 30 | return nil, fmt.Errorf("an error occurred loading the template stdlib libraries\n%w", err) 31 | } 32 | 33 | // models.types created by the compiled binary are different from models.types created by the interpreter at runtime. 34 | // pass the compiled models.types to the interpreter 35 | if err := i.Use(extract.Symbols); err != nil { 36 | return nil, fmt.Errorf("an error occurred loading the template models library\n%w", err) 37 | } 38 | 39 | // load the source 40 | if _, err := i.Eval(string(file)); err != nil { 41 | return nil, fmt.Errorf("an error occurred evaluating the template file\n%w", err) 42 | } 43 | 44 | // load the func from the interpreter 45 | v, err := i.Eval(symbol) 46 | if err != nil { 47 | return nil, fmt.Errorf("an error occurred evaluating a template function. Is it located in the file?\n%w", err) 48 | } 49 | 50 | return &v, nil 51 | } 52 | -------------------------------------------------------------------------------- /cli/generator/template/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 - 2025 SwitchUpCB 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 SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. -------------------------------------------------------------------------------- /cli/generator/template/generate.go: -------------------------------------------------------------------------------- 1 | // DO NOT CHANGE PACKAGE 2 | 3 | // Package template provides a template used by copygen to generate custom code. 4 | package template 5 | 6 | import ( 7 | "strings" 8 | 9 | "github.com/switchupcb/copygen/cli/models" 10 | ) 11 | 12 | // Generate generates code. 13 | // GENERATOR FUNCTION. 14 | // EDITABLE. 15 | // DO NOT REMOVE. 16 | func Generate(gen *models.Generator) (string, error) { 17 | var content strings.Builder 18 | 19 | content.WriteString(string(gen.Keep) + "\n") 20 | for i := range gen.Functions { 21 | content.WriteString(Function(&gen.Functions[i]) + "\n") 22 | } 23 | 24 | return content.String(), nil 25 | } 26 | 27 | // Function provides generated code for a function. 28 | func Function(function *models.Function) string { 29 | var fn strings.Builder 30 | fn.WriteString(generateComment(function) + "\n") 31 | fn.WriteString(generateSignature(function) + "\n") 32 | fn.WriteString(generateBody(function)) 33 | fn.WriteString(generateReturn(function)) 34 | return fn.String() 35 | } 36 | 37 | // generateComment generates a function comment. 38 | func generateComment(function *models.Function) string { 39 | var toComment strings.Builder 40 | for i, toType := range function.To { 41 | if i+1 == len(function.To) { 42 | toComment.WriteString(toType.Name()) 43 | break 44 | } 45 | 46 | toComment.WriteString(toType.Name() + ", ") 47 | } 48 | 49 | var fromComment strings.Builder 50 | for i, fromType := range function.From { 51 | if i+1 == len(function.From) { 52 | fromComment.WriteString(fromType.Name()) 53 | break 54 | } 55 | 56 | fromComment.WriteString(fromType.Name() + ", ") 57 | } 58 | 59 | return "// " + function.Name + " copies a " + fromComment.String() + " to a " + toComment.String() + "." 60 | } 61 | 62 | // generateSignature generates a function's signature. 63 | func generateSignature(function *models.Function) string { 64 | return "func " + function.Name + "(" + generateParameters(function) + ") {" 65 | } 66 | 67 | // generateParameters generates the parameters of a function. 68 | func generateParameters(function *models.Function) string { 69 | var parameters strings.Builder 70 | for _, toType := range function.To { 71 | parameters.WriteString(toType.Field.VariableName + " " + toType.Name() + ", ") 72 | } 73 | 74 | for i, fromType := range function.From { 75 | if i+1 == len(function.From) { 76 | parameters.WriteString(fromType.Field.VariableName + " " + fromType.Name()) 77 | break 78 | } 79 | 80 | parameters.WriteString(fromType.Field.VariableName + " " + fromType.Name() + ", ") 81 | } 82 | 83 | return parameters.String() 84 | } 85 | 86 | // generateBody generates the body of a function. 87 | func generateBody(function *models.Function) string { 88 | var body strings.Builder 89 | 90 | // Assign fields to ToType(s). 91 | for i, toType := range function.To { 92 | body.WriteString(generateAssignment(toType)) 93 | if i+1 != len(function.To) { 94 | body.WriteString("\n") 95 | } 96 | } 97 | 98 | return body.String() 99 | } 100 | 101 | // generateAssignment generates assignments for a to-type. 102 | func generateAssignment(toType models.Type) string { 103 | var assign strings.Builder 104 | assign.WriteString("// " + toType.Name() + " fields\n") 105 | 106 | for _, toField := range toType.Field.AllFields(nil, nil) { 107 | if toField.From != nil { 108 | assign.WriteString(toField.FullVariableName("") + " = ") 109 | 110 | fromField := toField.From 111 | if fromField.Options.Convert != "" { 112 | assign.WriteString(fromField.Options.Convert + "(" + fromField.FullVariableName("") + ")\n") 113 | } else if fromField.Options.Cast != "" { 114 | assign.WriteString(fromField.FullVariableName("") + "." + fromField.Options.Cast + "\n") 115 | } else { 116 | switch { 117 | case toField.FullDefinition() == fromField.FullDefinition(): 118 | assign.WriteString(fromField.FullVariableName("") + "\n") 119 | case toField.FullDefinition()[1:] == fromField.FullDefinition(): 120 | assign.WriteString("&" + fromField.FullVariableName("") + "\n") 121 | case toField.FullDefinition() == fromField.FullDefinition()[1:]: 122 | assign.WriteString("*" + fromField.FullVariableName("") + "\n") 123 | } 124 | } 125 | } 126 | } 127 | 128 | return assign.String() 129 | } 130 | 131 | // generateReturn generates a return statement for the function. 132 | func generateReturn(function *models.Function) string { 133 | return "}" 134 | } 135 | -------------------------------------------------------------------------------- /cli/matcher/matcher.go: -------------------------------------------------------------------------------- 1 | // Package matcher matches fields. 2 | package matcher 3 | 4 | import ( 5 | "github.com/switchupcb/copygen/cli/models" 6 | ) 7 | 8 | // Match matches the fields of a parsed generator. 9 | func Match(gen *models.Generator) error { 10 | for _, function := range gen.Functions { 11 | for _, toType := range function.To { 12 | for _, fromType := range function.From { 13 | 14 | // top-level types can be pointed (i.e domain.Account). 15 | toFields := toType.Field.AllFields(nil, nil) 16 | fromFields := fromType.Field.AllFields(nil, nil) 17 | 18 | // each toField is compared to every fromField. 19 | for i := 0; i < len(toFields); i++ { 20 | for j := 0; j < len(fromFields); j++ { 21 | match(function, toFields[i], fromFields[j]) 22 | if toFields[i].From != nil { 23 | break 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | RemoveUnpointedFields(gen) 32 | return nil 33 | } 34 | 35 | // match determines which matcher to use for two fields, then matches them. 36 | func match(function models.Function, toField *models.Field, fromField *models.Field) { 37 | if function.Options.Manual { 38 | switch { 39 | case toField.Options.Automatch || fromField.Options.Automatch: 40 | automatch(toField, fromField) 41 | 42 | case toField.Options.Tag != "": 43 | tagmatch(toField, fromField) 44 | 45 | default: 46 | mapmatch(toField, fromField) 47 | } 48 | } else { 49 | automatch(toField, fromField) 50 | } 51 | } 52 | 53 | // automatch automatically matches the fields of a fromType to a toType by name and definition. 54 | // automatch is used when no `map` or `tag` options apply to a field. 55 | func automatch(toField, fromField *models.Field) { 56 | if toField.Name == fromField.Name && 57 | ((toField.FullDefinition() == fromField.FullDefinition() || 58 | toField.FullDefinition()[1:] == fromField.FullDefinition() || 59 | toField.FullDefinition() == fromField.FullDefinition()[1:]) || 60 | fromField.Options.Convert != "") { 61 | fromField.To = toField 62 | toField.From = fromField 63 | 64 | // prevent parallel matching. 65 | fromField.Fields = make([]*models.Field, 0) 66 | toField.Fields = make([]*models.Field, 0) 67 | } 68 | } 69 | 70 | // mapmatch manually maps a from-field to a to-field. 71 | // mapmatch is used when a map option is specified. 72 | func mapmatch(toField, fromField *models.Field) { 73 | if fromField.Options.Map != "" && toField.FullNameWithoutPointer("") == fromField.Options.Map { 74 | fromField.To = toField 75 | toField.From = fromField 76 | } 77 | } 78 | 79 | // tagmatch manually maps a from-field to a to-field using tags. 80 | // tagmatch is used when a tag option is specified. 81 | func tagmatch(toField, fromField *models.Field) { 82 | if toField.Options.Tag != "" && toField.Options.Tag == fromField.Options.Tag { 83 | fromField.To = toField 84 | toField.From = fromField 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cli/matcher/relation.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | import ( 4 | "github.com/switchupcb/copygen/cli/models" 5 | ) 6 | 7 | // RemoveUnpointedFields removes unpointed fields from a Generator. 8 | func RemoveUnpointedFields(gen *models.Generator) { 9 | for _, function := range gen.Functions { 10 | for _, fromType := range function.From { 11 | fromType.Field.Fields = RelatedFields(fromType.Field.Fields, nil, nil) 12 | } 13 | 14 | for _, toType := range function.To { 15 | toType.Field.Fields = RelatedFields(toType.Field.Fields, nil, nil) 16 | } 17 | } 18 | } 19 | 20 | // RelatedFields returns solely related fields in a list of fields. 21 | func RelatedFields(fields, related []*models.Field, cyclic map[*models.Field]bool) []*models.Field { 22 | if cyclic == nil { 23 | cyclic = make(map[*models.Field]bool) 24 | } 25 | 26 | for _, subfield := range fields { 27 | if !cyclic[subfield] { 28 | cyclic[subfield] = true 29 | if len(subfield.Fields) != 0 { 30 | related = RelatedFields(subfield.Fields, related, cyclic) 31 | } 32 | 33 | if subfield.To != nil || subfield.From != nil { 34 | related = append(related, subfield) 35 | } 36 | } 37 | } 38 | 39 | return related 40 | } 41 | -------------------------------------------------------------------------------- /cli/models/category.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // IsType returns whether the field is a type. 4 | func (f *Field) IsType() bool { 5 | return f.Parent == nil 6 | } 7 | 8 | // basicMap contains a list of basic types. 9 | var ( 10 | basicMap = map[string]bool{ 11 | "invalid": true, 12 | "bool": true, 13 | "int": true, 14 | "int8": true, 15 | "int16": true, 16 | "int32": true, 17 | "int64": true, 18 | "uint": true, 19 | "uint8": true, 20 | "uint16": true, 21 | "uint32": true, 22 | "uint64": true, 23 | "uintptr": true, 24 | "float32": true, 25 | "float64": true, 26 | "complex64": true, 27 | "complex128": true, 28 | "string": true, 29 | "byte": true, 30 | "rune": true, 31 | } 32 | ) 33 | 34 | // IsBasic determines whether the field is a basic type. 35 | func (f *Field) IsBasic() bool { 36 | return basicMap[f.Definition] 37 | } 38 | 39 | // Pointer represents the char representation of a pointer. 40 | const Pointer = '*' 41 | 42 | // IsPointer returns whether the field is a pointer. 43 | func (f *Field) IsPointer() bool { 44 | return len(f.Definition) >= 1 && f.Definition[0] == Pointer 45 | } 46 | 47 | // Collection refers to a category of types which indicate that 48 | // a field's definition collects multiple fields (i.e `map[string]bool`). 49 | const ( 50 | CollectionPointer = "*" 51 | CollectionSlice = "[]" 52 | CollectionMap = "map" 53 | CollectionChan = "chan" 54 | CollectionFunc = "func" 55 | CollectionInterface = "interface" 56 | ) 57 | 58 | // IsArray returns whether the field is an array. 59 | func (f *Field) IsArray() bool { 60 | return len(f.Definition) >= 3 && f.Definition[0] == '[' && ('0' <= f.Definition[1] && f.Definition[1] <= '9') 61 | } 62 | 63 | // IsSlice returns whether the field is a slice. 64 | func (f *Field) IsSlice() bool { 65 | return len(f.Definition) >= 2 && f.Definition[:2] == CollectionSlice 66 | } 67 | 68 | // IsMap returns whether the field is a map. 69 | func (f *Field) IsMap() bool { 70 | return len(f.Definition) >= 3 && f.Definition[:3] == CollectionMap 71 | } 72 | 73 | // IsMap returns whether the field is a chan. 74 | func (f *Field) IsChan() bool { 75 | return len(f.Definition) >= 4 && f.Definition[:4] == CollectionChan 76 | } 77 | 78 | // IsComposite returns whether the field is a composite type: array, slice, map, chan. 79 | func (f *Field) IsComposite() bool { 80 | return f.IsArray() || f.IsSlice() || f.IsMap() || f.IsChan() 81 | } 82 | 83 | // IsFunc returns whether the field is a function. 84 | func (f *Field) IsFunc() bool { 85 | return len(f.Definition) >= 4 && f.Definition[:4] == CollectionFunc 86 | } 87 | 88 | // IsInterface returns whether the field is an interface. 89 | func (f *Field) IsInterface() bool { 90 | if f.Underlying != nil { 91 | return f.Underlying.IsInterface() 92 | } 93 | 94 | return len(f.Definition) >= 9 && f.Definition[:9] == CollectionInterface 95 | } 96 | 97 | // IsCollection returns whether the field is a collection. 98 | func (f *Field) IsCollection() bool { 99 | return f.IsPointer() || f.IsComposite() || f.IsFunc() || f.IsInterface() 100 | } 101 | 102 | // IsAlias determines whether the field is a type alias. 103 | func (f *Field) IsAlias() bool { 104 | return f.Definition != "" && !(f.IsBasic() || f.IsPointer() || f.IsComposite() || f.IsFunc()) 105 | } 106 | -------------------------------------------------------------------------------- /cli/models/debug/debug.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/switchupcb/copygen/cli/models" 7 | ) 8 | 9 | // PrintGeneratorFields prints all of a generator's function's fields to standard output. 10 | func PrintGeneratorFields(gen *models.Generator) { 11 | for i := 0; i < len(gen.Functions); i++ { 12 | fmt.Println(gen.Functions[i].Name, "{") 13 | PrintFunctionFields(&gen.Functions[i]) 14 | fmt.Println("}") 15 | fmt.Println() 16 | } 17 | } 18 | 19 | // PrintFunctionFields prints all of a function's fields to standard output. 20 | func PrintFunctionFields(function *models.Function) { 21 | for i := 0; i < len(function.From); i++ { 22 | fmt.Println(function.From[i]) 23 | PrintFieldGraph(function.From[i].Field, "\t", nil) 24 | } 25 | 26 | for i := 0; i < len(function.To); i++ { 27 | fmt.Println(function.To[i]) 28 | PrintFieldGraph(function.To[i].Field, "\t", nil) 29 | } 30 | } 31 | 32 | // PrintFieldGraph prints a list of fields with the related fields. 33 | func PrintFieldGraph(field *models.Field, tabs string, cyclic map[*models.Field]bool) { 34 | if cyclic == nil { 35 | cyclic = make(map[*models.Field]bool) 36 | } 37 | 38 | fmt.Printf("%v%v\n", tabs, field) 39 | cyclic[field] = true 40 | for _, subfield := range field.Fields { 41 | if !cyclic[subfield] { 42 | PrintFieldGraph(subfield, tabs+"\t", cyclic) 43 | } 44 | } 45 | } 46 | 47 | // PrintFieldTree prints a tree of fields for a given type field to standard output. 48 | func PrintFieldTree(field *models.Field, tabs string, cyclic map[*models.Field]bool) { 49 | if cyclic == nil { 50 | cyclic = make(map[*models.Field]bool) 51 | } 52 | 53 | if tabs == "" { 54 | fmt.Println(tabs + "type " + field.FullDefinition()) 55 | } else { 56 | fmt.Println(tabs + field.Name + "\t" + field.FullDefinition()) 57 | } 58 | cyclic[field] = true 59 | 60 | tabs += "\t" // field tab 61 | for _, subfield := range field.Fields { 62 | if !cyclic[subfield] { 63 | PrintFieldTree(subfield, tabs+"\t", cyclic) 64 | } 65 | } 66 | } 67 | 68 | // PrintFieldRelation prints the relationship between a list of to and from fields. 69 | func PrintFieldRelation(toFields, fromFields []*models.Field) { 70 | for _, toField := range toFields { 71 | for _, fromField := range fromFields { 72 | printFieldRelation(toField, fromField) 73 | } 74 | } 75 | } 76 | 77 | // printFieldRelation prints the relationship between two fields. 78 | func printFieldRelation(toField, fromField *models.Field) { 79 | switch { 80 | case toField.From == fromField && fromField.To == toField: 81 | fmt.Printf("%v and %v are related to each other.\n", toField, fromField) 82 | case toField.From == fromField: 83 | fmt.Printf("%v is related to %v.\n", toField, fromField) 84 | case fromField.To == toField: 85 | fmt.Printf("%v is related to %v.\n", toField, fromField) 86 | default: 87 | fmt.Printf("%v is not related to %v.\n", toField, fromField) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cli/models/field.go: -------------------------------------------------------------------------------- 1 | // Package models defines the domain models that model field relations and manipulation. 2 | package models 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | // Field represents a field to be copied to/from. 9 | // A field's struct properties are set in the parser unless its stated otherwise. 10 | type Field struct { 11 | // VariableName represents name that is used to assign the field. 12 | // 13 | // This value will always be unique in the context of the application. 14 | // TypeField variable names do not contain '.' (i.e 'tA' in 'tA.UserID'). 15 | // Field variable names are defined by their specifier (i.e '.UserID' in 'domain.Account.UserID'). 16 | VariableName string 17 | 18 | // Import represents the file that field was imported from. 19 | Import string 20 | 21 | // Package represents the package the field is defined in (i.e `log` in `log.Logger`). 22 | Package string 23 | 24 | // Name represents the name of the field (i.e `ID` in `ID int`). 25 | Name string 26 | 27 | // Definition represents the type definition of the field (i.e `int` in `ID int`, `Logger` in `log.Logger`). 28 | Definition string 29 | 30 | // The tags defined in a struct field (i.e `json:"tag,omitempty"`) 31 | // map[tag]map[name][]options (i.e map[json]map[tag]["omitempty"]) 32 | Tags map[string]map[string][]string 33 | 34 | // The type or field that contains this field. 35 | Parent *Field 36 | 37 | // Underlying represents the underlying type field of this field (or nil). 38 | // The underlying field of a `type Number string` *Field is a `string` *Field. 39 | // 40 | // Underlying fields of the same type point to the same *Field object. 41 | Underlying *Field 42 | 43 | // The field that this field is copied from (or nil). 44 | // 45 | // Set in the matcher. 46 | From *Field 47 | 48 | // The field that this field is copied to (or nil). 49 | // 50 | // Set in the matcher. 51 | To *Field 52 | 53 | // The fields of this field. 54 | Fields []*Field 55 | 56 | // The custom options of a field. 57 | Options FieldOptions 58 | 59 | // Embedded represents whether the field is an embedded field. 60 | Embedded bool 61 | } 62 | 63 | // FieldOptions represent options for a Field. 64 | type FieldOptions struct { 65 | // The function the field is casted with. 66 | Cast string 67 | 68 | // The function the field is converted with (as a parameter). 69 | Convert string 70 | 71 | // The field to map this field to, if any. 72 | Map string 73 | 74 | // The tag to map this field with, if any. 75 | Tag string 76 | 77 | // The level at which sub-fields are discovered. 78 | Depth int 79 | 80 | // Whether the field should be explicitly automatched. 81 | Automatch bool 82 | 83 | // Whether the field should be deepcopied. 84 | Deepcopy bool 85 | } 86 | 87 | // Deepcopy returns a new field with copied properties (excluding Parent, To, and From fields). 88 | func (f *Field) Deepcopy(cyclic map[*Field]bool) *Field { 89 | copied := &Field{ 90 | VariableName: f.VariableName, 91 | Import: f.Import, 92 | Package: f.Package, 93 | Name: f.Name, 94 | Definition: f.Definition, 95 | Underlying: f.Underlying, 96 | Options: FieldOptions{ 97 | Cast: f.Options.Cast, 98 | Convert: f.Options.Convert, 99 | Map: f.Options.Map, 100 | Tag: f.Options.Tag, 101 | Depth: f.Options.Depth, 102 | Automatch: f.Options.Automatch, 103 | Deepcopy: f.Options.Deepcopy, 104 | }, 105 | Embedded: f.Embedded, 106 | } 107 | 108 | copied.Tags = make(map[string]map[string][]string, len(f.Tags)) 109 | for k1, mapval := range f.Tags { 110 | copied.Tags[k1] = make(map[string][]string, len(mapval)) 111 | for k2, sliceval := range f.Tags[k1] { 112 | copied.Tags[k1][k2] = make([]string, len(sliceval)) 113 | copy(copied.Tags[k1][k2], f.Tags[k1][k2]) 114 | } 115 | } 116 | 117 | // setup the cache 118 | if cyclic == nil { 119 | cyclic = make(map[*Field]bool) 120 | } 121 | 122 | // copy the subfields. 123 | cyclic[f] = true 124 | copied.Fields = make([]*Field, len(f.Fields)) 125 | for i, sf := range f.Fields { 126 | if cyclic[sf] { 127 | copied.Fields[i] = sf 128 | continue 129 | } 130 | 131 | copied.Fields[i] = sf.Deepcopy(cyclic) 132 | copied.Fields[i].Parent = copied 133 | } 134 | 135 | return copied 136 | } 137 | 138 | // AllFields gets all the fields in the scope of a field (including itself). 139 | func (f *Field) AllFields(fields []*Field, cyclic map[*Field]bool) []*Field { 140 | if cyclic == nil { 141 | cyclic = make(map[*Field]bool) 142 | } 143 | 144 | fields = append(fields, f) 145 | cyclic[f] = true 146 | for _, subfield := range f.Fields { 147 | if !cyclic[subfield] { 148 | fields = subfield.AllFields(fields, cyclic) 149 | } 150 | } 151 | 152 | return fields 153 | } 154 | 155 | // FullVariableName returns the full variable name of a field (i.e tA.User.UserID). 156 | func (f *Field) FullVariableName(name string) string { 157 | if !f.IsType() { 158 | return f.Parent.FullVariableName(f.VariableName + name) 159 | } 160 | 161 | return f.VariableName + name 162 | } 163 | 164 | // FullDefinition returns the full definition of a field including its package 165 | // without its pointer(s) (i.e domain.Account). 166 | func (f *Field) FullDefinitionWithoutPointer() string { 167 | i := 0 168 | for i < len(f.Definition) && f.Definition[i] == Pointer { 169 | i++ 170 | } 171 | 172 | if f.Package == "" { 173 | return f.Definition[i:] 174 | } 175 | 176 | return f.Package + "." + f.Definition[i:] 177 | } 178 | 179 | // FullDefinition returns the full definition of a field including its package. 180 | func (f *Field) FullDefinition() string { 181 | if f.Package == "" { 182 | return f.Definition 183 | } 184 | 185 | i := 0 186 | for i < len(f.Definition) && f.Definition[i] == Pointer { 187 | i++ 188 | } 189 | 190 | return f.Definition[:i] + f.Package + "." + f.Definition[i:] 191 | } 192 | 193 | // FullNameWithoutPointer returns the full name of a field including its parents 194 | // without the pointer (i.e domain.Account.User.ID). 195 | func (f *Field) FullNameWithoutPointer(name string) string { 196 | if !f.IsType() { 197 | // names are added in reverse. 198 | if name == "" { 199 | // reference the field (i.e `ID`). 200 | name = f.Name 201 | } else { 202 | // prepend the field (i.e `User` + `.` + `ID`). 203 | name = f.Name + "." + name 204 | } 205 | 206 | return f.Parent.FullNameWithoutPointer(name) 207 | } 208 | 209 | if name != "" { 210 | name = "." + name 211 | } 212 | 213 | return f.FullDefinitionWithoutPointer() + name 214 | } 215 | 216 | // FullName returns the full name of a field including its parents (i.e *domain.Account.User.ID). 217 | func (f *Field) FullName() string { 218 | i := 0 219 | for i < len(f.Definition) && f.Definition[i] == Pointer { 220 | i++ 221 | } 222 | 223 | return f.Definition[:i] + f.FullNameWithoutPointer("") 224 | } 225 | 226 | func (f *Field) String() string { 227 | direction := "Unpointed" 228 | if f.From != nil { 229 | direction = "To" 230 | } 231 | 232 | if f.To != nil { 233 | switch direction { 234 | case "To": 235 | direction = "To and From" 236 | case "Unpointed": 237 | direction = "From" 238 | } 239 | } 240 | 241 | var name string 242 | if f.Name != "" { 243 | name = f.Name + " " 244 | } 245 | 246 | var parent string 247 | if f.Parent != nil { 248 | parent = f.Parent.FullName() 249 | } 250 | 251 | return fmt.Sprintf("%v Field %v%q of Definition %q Fields[%v]: Parent %q", direction, name, f.FullName(), f.FullDefinition(), len(f.Fields), parent) 252 | } 253 | -------------------------------------------------------------------------------- /cli/models/function.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Function represents the properties of a generated function. 4 | type Function struct { 5 | Name string // The name of the function. 6 | Options FunctionOptions // The custom options of a function. 7 | From []Type // The types to copy fields from. 8 | To []Type // The types to copy fields to. 9 | } 10 | 11 | // FunctionOptions represent options for a Function. 12 | type FunctionOptions struct { 13 | Custom map[string][]string // The custom options of a function (map[option]values). 14 | Manual bool // Whether the function uses a manual matcher (as opposed to an Automatcher). 15 | } 16 | -------------------------------------------------------------------------------- /cli/models/generator.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Generator represents a code generator. 4 | type Generator struct { 5 | Functions []Function // The functions to generate. 6 | Options GeneratorOptions // The custom options for the generator. 7 | Setpath string // The filepath the setup file is located in. 8 | Outpath string // The filepath the generated code is output to. 9 | Tempath string // The filepath for the template used to generate code. 10 | Keep []byte // The code that is kept from the setup file. 11 | } 12 | 13 | // GeneratorOptions represents options for a Generator. 14 | type GeneratorOptions struct { 15 | Custom map[string]interface{} // The custom options of a generator. 16 | Matcher MatcherOptions // The options for the matcher of a generator. 17 | } 18 | 19 | // MatcherOptions represents options for the Generator's matcher. 20 | type MatcherOptions struct { 21 | CastDepth int // The option that sets the maximum depth for automatic casting. 22 | Skip bool // The option that skips the matcher. 23 | AutoCast bool // The option that enables automatic casting. 24 | DisableAssignObjectInterface bool // The cast option feature flag that disables assignment of objects to interfaces. 25 | DisableAssertInterfaceObject bool // The cast option feature flag that disables assignment of interfaces to objects. 26 | DisableConvert bool // The cast option feature flag that disables type conversion. 27 | } 28 | -------------------------------------------------------------------------------- /cli/models/type.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "fmt" 4 | 5 | // Type represents a field that isn't contained. 6 | type Type struct { 7 | // Field represents field information for the type. 8 | Field *Field 9 | } 10 | 11 | // Name gets the name of the type field. 12 | func (t Type) Name() string { 13 | return t.Field.FullName() 14 | } 15 | 16 | func (t Type) String() string { 17 | return fmt.Sprintf("type %v", t.Field.FullName()) 18 | } 19 | -------------------------------------------------------------------------------- /cli/parser/ast.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | ) 7 | 8 | const copygenInterfaceName = "Copygen" 9 | 10 | // assertCopygenInterface determines if an ast.GenDecl is a Copygen Interface by type assertion. 11 | func assertCopygenInterface(x *ast.GenDecl) (*ast.InterfaceType, bool) { 12 | if x.Tok == token.TYPE { 13 | for _, spec := range x.Specs { 14 | if ts, ok := spec.(*ast.TypeSpec); ok { 15 | if it, ok := ts.Type.(*ast.InterfaceType); ok && ts.Name.Name == copygenInterfaceName { 16 | return it, true 17 | } 18 | } 19 | } 20 | } 21 | 22 | return nil, false 23 | } 24 | 25 | // getNodeComments returns all of the ast.Comments in a given node. 26 | func getNodeComments(x ast.Node) []*ast.Comment { 27 | var optionComments []*ast.Comment 28 | 29 | ast.Inspect(x, func(node ast.Node) bool { 30 | commentGroup, ok := node.(*ast.CommentGroup) 31 | if !ok { 32 | return true 33 | } 34 | 35 | for i := 0; i < len(commentGroup.List); i++ { 36 | optionComments = append(optionComments, commentGroup.List[i]) 37 | } 38 | 39 | return true 40 | }) 41 | 42 | return optionComments 43 | } 44 | 45 | const ( 46 | newline = 1 47 | carriagereturn = 2 48 | ) 49 | 50 | // astRemoveComments removes ast.Comments from an *ast.File. 51 | func astRemoveComments(file *ast.File, comments []*ast.Comment) { 52 | // remove comments starting from the bottom of the file. 53 | for i := len(file.Comments) - 1; i > -1; i-- { 54 | fileCommentGroup := file.Comments[i] 55 | 56 | // remove comments from the bottom of each comment group. 57 | for j := len(fileCommentGroup.List) - 1; j > -1; j-- { 58 | fileComment := fileCommentGroup.List[j] 59 | 60 | for k := len(comments) - 1; k > -1; k-- { 61 | comment := comments[k] 62 | 63 | // remove the comment. 64 | if fileComment == comment { 65 | // reslice the commentGroup to remove the comment. 66 | fileCommentGroup.List = append(fileCommentGroup.List[:j], fileCommentGroup.List[j+1:]...) 67 | 68 | // prevent free-floating comments. 69 | if j != 0 && 70 | (fileCommentGroup.List[j-1].End()+newline == comment.Slash || 71 | fileCommentGroup.List[j-1].End()+carriagereturn == comment.Slash) { 72 | fileCommentGroup.List[j-1].Slash = comment.Slash 73 | } 74 | 75 | // prevent the comment from being compared again. 76 | comments[k] = comments[len(comments)-1] 77 | comments = comments[:len(comments)-1] 78 | 79 | break 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cli/parser/field.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/fatih/structtag" 10 | "github.com/switchupcb/copygen/cli/models" 11 | ) 12 | 13 | // parseField parses a types.Type into a *models.Field recursively. 14 | func parseField(typ types.Type) *models.Field { 15 | if cached, ok := fieldcache[typ.String()]; ok { 16 | return cached 17 | } 18 | 19 | field := new(models.Field) 20 | switch x := typ.(type) { 21 | 22 | // Named Types (Alias) 23 | // https://go.googlesource.com/example/+/HEAD/gotypes#named-types 24 | case *types.Named: 25 | // set the cache early to prevent issues with named cyclic types. 26 | fieldcache[x.String()] = field 27 | 28 | // A named type is either: 29 | // 1. an alias (i.e `Placeholder` in `type Placeholder bool`) 30 | // 2. a struct (i.e `Account` in `type Account struct`) 31 | // 3. an interface (i.e `error` in `type error interface`) 32 | // 4. a collected type (i.e `domain.Account` in `[]domain.Account`) 33 | // 34 | // Underlying named types are only important in case 2, 35 | // when we need to parse extra information from the field. 36 | if xs, ok := x.Underlying().(*types.Struct); ok { 37 | structfield := parseField(xs) 38 | field.Fields = structfield.Fields 39 | } else { 40 | field.Underlying = parseField(x.Underlying()) 41 | } 42 | 43 | field.Definition = x.Obj().Name() 44 | setFieldImportAndPackage(field, x.Obj().Pkg()) 45 | 46 | // Basic Types 47 | // https://go.googlesource.com/example/+/HEAD/gotypes#basic-types 48 | case *types.Basic: 49 | field.Definition = x.Name() 50 | 51 | // Simple Composite Types 52 | // https://go.googlesource.com/example/+/HEAD/gotypes#simple-composite-types 53 | case *types.Pointer: 54 | elemfield := parseField(x.Elem()) 55 | 56 | // type aliases (including structs) must be deepcopied 57 | // in order to match underlying fields. 58 | if elemfield.IsAlias() { 59 | deepfield := elemfield.Deepcopy(nil) 60 | field.Fields = deepfield.Fields 61 | } 62 | 63 | field.Definition = models.CollectionPointer + collectedDefinition(elemfield) 64 | field.VariableName = "." + alphastring(elemfield.Definition) 65 | 66 | case *types.Array: 67 | field.Definition = "[" + strconv.FormatInt(x.Len(), 10) + "]" + collectedDefinition(parseField(x.Elem())) 68 | 69 | case *types.Slice: 70 | field.Definition = models.CollectionSlice + collectedDefinition(parseField(x.Elem())) 71 | 72 | case *types.Map: 73 | field.Definition = models.CollectionMap + "[" + collectedDefinition(parseField(x.Key())) + "]" + collectedDefinition(parseField(x.Elem())) 74 | 75 | case *types.Chan: 76 | field.Definition = models.CollectionChan + " " + collectedDefinition(parseField(x.Elem())) 77 | 78 | // Function (without Receivers) 79 | // https://go.googlesource.com/example/+/HEAD/gotypes#function-and-method-types 80 | case *types.Signature: 81 | var definition strings.Builder 82 | 83 | // set the parameters. 84 | definition.WriteString(models.CollectionFunc + "(") 85 | for i := 0; i < x.Params().Len(); i++ { 86 | definition.WriteString(collectedDefinition(parseField(x.Params().At(i).Type()))) 87 | if i+1 != x.Params().Len() { 88 | definition.WriteString(", ") 89 | } 90 | } 91 | definition.WriteString(")") 92 | 93 | // set the results. 94 | if x.Results().Len() >= 1 { 95 | definition.WriteString(" ") 96 | } 97 | if x.Results().Len() > 1 { 98 | definition.WriteString("(") 99 | } 100 | for i := 0; i < x.Results().Len(); i++ { 101 | definition.WriteString(collectedDefinition(parseField(x.Results().At(i).Type()))) 102 | if i+1 != x.Results().Len() { 103 | definition.WriteString(", ") 104 | } 105 | } 106 | if x.Results().Len() > 1 { 107 | definition.WriteString(")") 108 | } 109 | 110 | field.Definition = definition.String() 111 | 112 | // Interface Types 113 | // https://go.googlesource.com/example/+/HEAD/gotypes#interface-types 114 | case *types.Interface: 115 | if x.Empty() { 116 | field.Definition = x.String() 117 | } else { 118 | var definition strings.Builder 119 | definition.WriteString(models.CollectionInterface + "{") 120 | 121 | for i := 0; i < x.NumMethods(); i++ { 122 | definition.WriteString(collectedDefinition(parseField(x.Method(i).Type())) + "; ") 123 | } 124 | 125 | for i := 0; i < x.NumEmbeddeds(); i++ { 126 | definition.WriteString(collectedDefinition(parseField(x.EmbeddedType(i))) + "; ") 127 | } 128 | 129 | definition.WriteString("}") 130 | field.Definition = definition.String() 131 | } 132 | 133 | // Struct Types 134 | // https://go.googlesource.com/example/+/HEAD/gotypes#struct-types 135 | case *types.Struct: 136 | var definition strings.Builder 137 | definition.WriteString("struct{") 138 | for i := 0; i < x.NumFields(); i++ { 139 | // a deepcopy of subfield is returned, then modified. 140 | subfield := parseField(x.Field(i).Type()).Deepcopy(nil) 141 | subfield.VariableName = "." + x.Field(i).Name() 142 | subfield.Name = x.Field(i).Name() 143 | setTags(subfield, x.Tag(i)) 144 | subfield.Parent = field 145 | field.Fields = append(field.Fields, subfield) 146 | 147 | if x.Field(i).Embedded() { 148 | subfield.Embedded = true 149 | } 150 | 151 | definition.WriteString(subfield.Name + " " + subfield.FullDefinition() + "; ") 152 | 153 | // Due to the possibility of cyclic structs, 154 | // all subfields are deepcopied with len([]Fields) == (0:?). 155 | // 156 | // In order to correctly represent a deepcopied subfield, 157 | // point its fields back to the cached field []Fields, 158 | // which are eventually filled. 159 | // 160 | // cachedsubfield.Fields pointer is never modified. 161 | if cachedsubfield, ok := fieldcache[x.Field(i).String()]; ok { 162 | subfield.Fields = cachedsubfield.Fields 163 | } 164 | } 165 | definition.WriteString("}") 166 | field.Definition = definition.String() 167 | 168 | default: 169 | fmt.Printf("WARNING: could not parse type %v\n", x.String()) 170 | } 171 | 172 | return field 173 | } 174 | 175 | // setFieldImportAndPackage sets the import and package of a field. 176 | func setFieldImportAndPackage(field *models.Field, pkg *types.Package) { 177 | if pkg == nil { 178 | return 179 | } 180 | 181 | field.Import = pkg.Path() 182 | field.Package = pkg.Name() 183 | } 184 | 185 | // setTags sets the tags for a field. 186 | func setTags(field *models.Field, rawtag string) { 187 | // rawtag represents tags as they are defined (i.e `api:"id", json:"tag"`). 188 | tags, err := structtag.Parse(rawtag) 189 | if err != nil { 190 | fmt.Printf("WARNING: could not parse tag for field %v\n%v", field.FullName(), err) 191 | } 192 | 193 | field.Tags = make(map[string]map[string][]string, tags.Len()) 194 | for _, tag := range tags.Tags() { 195 | field.Tags[tag.Key] = map[string][]string{ 196 | tag.Name: tag.Options, 197 | } 198 | } 199 | } 200 | 201 | // collectedDefinition determines the full definition for a collected type in a collection. 202 | // 203 | // collectedDefinition can be called in the parser, but ONLY because collections are NOT cached. 204 | func collectedDefinition(collected *models.Field) string { 205 | // a generated file's package == setup file's package. 206 | // 207 | // when the field is defined in the setup file (i.e `Collection`), 208 | // it is parsed with the setup file's package (i.e `copygen.Collection`). 209 | // 210 | // do NOT reference it by package in the generated file (i.e `Collection`). 211 | if collected.Import == setupPkgPath { 212 | return collected.Definition 213 | } 214 | 215 | // when a setup file imports the package it will output to, 216 | // do NOT reference the fields defined in the output package, by package. 217 | if outputPkgPath != "" && collected.Import == outputPkgPath { 218 | return collected.Definition 219 | } 220 | 221 | // when a field's import uses an alias, reassign the package reference. 222 | if aliasPkg, ok := aliasImportMap[collected.Import]; ok { 223 | return aliasPkg + "." + collected.Definition 224 | } 225 | 226 | return collected.FullDefinition() 227 | } 228 | -------------------------------------------------------------------------------- /cli/parser/function.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "go/types" 8 | 9 | "github.com/switchupcb/copygen/cli/models" 10 | "github.com/switchupcb/copygen/cli/parser/options" 11 | ) 12 | 13 | // parseFunctions parses the AST for functions in the setup file. 14 | // The copygen *ast.InterfaceType is used to assign options from *ast.Comments. 15 | func (p *Parser) parseFunctions(copygen *ast.InterfaceType) ([]models.Function, error) { 16 | numMethods := len(copygen.Methods.List) 17 | if numMethods == 0 { 18 | fmt.Println("WARNING: no functions are defined in the \"type Copygen interface\"") 19 | } 20 | 21 | // create models.Function objects. 22 | functions := make([]models.Function, numMethods) 23 | for i := 0; i < numMethods; i++ { 24 | method := p.Config.SetupPkg.TypesInfo.Defs[copygen.Methods.List[i].Names[0]] 25 | 26 | // create models.Type objects. 27 | fieldoptions, manual := getNodeOptions(copygen.Methods.List[i], p.Options.CommentOptionMap) 28 | fieldoptions = append(fieldoptions, p.Options.ConvertOptions...) 29 | 30 | methodFuncs, ok := method.(*types.Func) 31 | if !ok { 32 | return nil, errors.New("method object is not a Go types function") 33 | } 34 | 35 | parsed, err := parseTypes(methodFuncs) 36 | if err != nil { 37 | return nil, fmt.Errorf("an error occurred while parsing the types of function %q.\n%w", method.Name(), err) 38 | } 39 | 40 | // set the options for each field. 41 | setTypeOptions(parsed.fromTypes, fieldoptions) 42 | setTypeOptions(parsed.toTypes, fieldoptions) 43 | 44 | // map the function custom options. 45 | customoptionmap := make(map[string][]string) 46 | for _, option := range fieldoptions { 47 | customoptionmap, err = options.MapCustomOption(customoptionmap, option) 48 | if err != nil { 49 | fmt.Printf("WARNING: %v\n", err) 50 | } 51 | } 52 | 53 | // create the models.Function object. 54 | function := models.Function{ 55 | Name: method.Name(), 56 | To: parsed.toTypes, 57 | From: parsed.fromTypes, 58 | Options: models.FunctionOptions{ 59 | Custom: customoptionmap, 60 | Manual: manual, 61 | }, 62 | } 63 | 64 | functions[i] = function 65 | } 66 | 67 | return functions, nil 68 | } 69 | 70 | // getNodeOptions gets an ast.Node options from its comments. 71 | // To reduce overhead, it also returns whether a manual matcher is used. 72 | func getNodeOptions(x ast.Node, commentoptionmap map[string]*options.Option) ([]*options.Option, bool) { 73 | nodeOptions := make([]*options.Option, 0, len(commentoptionmap)) 74 | var manual bool 75 | 76 | ast.Inspect(x, func(node ast.Node) bool { 77 | commentGroup, ok := node.(*ast.CommentGroup) 78 | if !ok { 79 | return true 80 | } 81 | 82 | for _, comment := range commentGroup.List { 83 | if commentoptionmap[comment.Text] != nil { 84 | nodeOptions = append(nodeOptions, commentoptionmap[comment.Text]) 85 | 86 | // specifying a match option disables automatching by default. 87 | if options.IsMatchOptionCategory(commentoptionmap[comment.Text].Category) { 88 | manual = true 89 | } 90 | } 91 | } 92 | 93 | return true 94 | }) 95 | 96 | return nodeOptions, manual 97 | } 98 | 99 | // setTypeOptions sets the options for all fields in the given types. 100 | func setTypeOptions(types []models.Type, fieldoptions []*options.Option) { 101 | for _, t := range types { 102 | for _, field := range t.Field.AllFields(nil, nil) { 103 | options.SetFieldOptions(field, fieldoptions) 104 | options.FilterDepth(field, field.Options.Depth, 0) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cli/parser/keep.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "strings" 8 | 9 | "github.com/switchupcb/copygen/cli/parser/options" 10 | ) 11 | 12 | const convertOptionSplitAmount = 3 13 | 14 | // Keep removes ast.Nodes from an ast.File that are kept in a generated output file. 15 | func (p *Parser) Keep(astFile *ast.File) error { 16 | var foundCopygenInterface bool 17 | var trash []*ast.Comment 18 | 19 | for i := len(astFile.Decls) - 1; i > -1; i-- { 20 | switch declaration := astFile.Decls[i].(type) { 21 | case *ast.GenDecl: 22 | 23 | // keep all declaration objects in the setup file except for the `type Copygen interface`. 24 | if _, ok := assertCopygenInterface(declaration); ok { 25 | foundCopygenInterface = true 26 | 27 | // remove from the `type Copygen interface` (from the slice). 28 | astFile.Decls[i] = astFile.Decls[len(astFile.Decls)-1] 29 | astFile.Decls = astFile.Decls[:len(astFile.Decls)-1] 30 | 31 | // remove the `type Copygen interface` function ast.Comments. 32 | comments := getNodeComments(declaration) 33 | if err := p.assignFieldOption(comments); err != nil { 34 | return fmt.Errorf("%w", err) 35 | } 36 | trash = append(trash, comments...) 37 | } 38 | 39 | case *ast.FuncDecl: 40 | comments, err := p.assignConvertOptions(declaration) 41 | if err != nil { 42 | return fmt.Errorf("%w", err) 43 | } 44 | 45 | // remove convert option ast.Comments. 46 | trash = append(trash, comments...) 47 | } 48 | } 49 | 50 | // Remove ast.Comments that are parsed into options from the ast.File. 51 | astRemoveComments(astFile, trash) 52 | 53 | if !foundCopygenInterface { 54 | return errors.New("the \"type Copygen interface\" could not be found (in the setup file's AST)") 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // assignFieldOption parses a list of ast.Comments into options 61 | // and places them in a map[text]Option. 62 | func (p *Parser) assignFieldOption(comments []*ast.Comment) error { 63 | if p.Options.CommentOptionMap == nil { 64 | p.Options.CommentOptionMap = make(map[string]*options.Option, len(comments)) 65 | } 66 | 67 | for _, comment := range comments { 68 | text := comment.Text 69 | 70 | // do NOT parse comments that have already been parsed. 71 | if p.Options.CommentOptionMap[text] != nil { 72 | continue 73 | } 74 | 75 | splitcomments := strings.Fields(text[2:]) 76 | if len(splitcomments) >= 1 { 77 | 78 | category := splitcomments[0] 79 | if category == options.CategoryConvert { 80 | continue 81 | } 82 | 83 | optiontext := strings.Join(splitcomments[1:], " ") 84 | option, err := options.NewFieldOption(category, optiontext) 85 | if err != nil { 86 | return fmt.Errorf("%w", err) 87 | } 88 | 89 | p.Options.CommentOptionMap[text] = option 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // assignConvertOptions initializes convert options. 97 | // Used in the context of functions other than the type Copygen interface. 98 | func (p *Parser) assignConvertOptions(x *ast.FuncDecl) ([]*ast.Comment, error) { 99 | var ( 100 | convertComments []*ast.Comment 101 | assignErr error 102 | ) 103 | 104 | ast.Inspect(x, func(node ast.Node) bool { 105 | commentGroup, ok := node.(*ast.CommentGroup) 106 | if !ok { 107 | return true 108 | } 109 | 110 | for _, comment := range commentGroup.List { 111 | text := comment.Text 112 | splitcomments := strings.Fields(text[2:]) 113 | 114 | // determine if the comment is a convert option. 115 | if len(splitcomments) == convertOptionSplitAmount { 116 | category := splitcomments[0] 117 | value := strings.Join(splitcomments[1:], " ") 118 | if category == options.CategoryConvert { 119 | option, err := options.ParseConvert(value, x.Name.Name) 120 | if err != nil { 121 | assignErr = err 122 | return false 123 | } 124 | 125 | p.Options.ConvertOptions = append(p.Options.ConvertOptions, option) 126 | convertComments = append(convertComments, comment) 127 | } 128 | } 129 | } 130 | 131 | return true 132 | }) 133 | 134 | return convertComments, assignErr 135 | } 136 | -------------------------------------------------------------------------------- /cli/parser/options/cast.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/switchupcb/copygen/cli/models" 9 | ) 10 | 11 | const ( 12 | CategoryCast = "cast" 13 | 14 | // FormatCast represents an end-user facing format for a cast option. 15 | //