├── .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 | //