diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 5023777..3523422 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -18,11 +18,11 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.23' - name: Perform static code analysis via golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.52.1 + version: v1.64.5 Integration: needs: Linter @@ -40,7 +40,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.23' - name: Run tests working-directory: ./examples run: go test ./_tests diff --git a/.golangci.yml b/.golangci.yml index 4ff591c..25bcca1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,19 +5,20 @@ run: linters: enable-all: true disable: - - gomnd # Run manually prior to a release. - gomoddirectives # The repository uses go modules in its interpreter functionality from a temporary tagged fork. - - cyclop + - cyclop + - depguard + - err113 # future implementation - exhaustruct - - exhaustivestruct # exhausting - forbidigo - funlen # can't handle switch statement - gochecknoglobals # No data race conditions - gocognit - - goerr113 # errors used once - gofumpt # too many false positives + - intrange - lll + - mnd - nestif # can't handle cache - nlreturn - unparam # generator function @@ -25,15 +26,8 @@ linters: - whitespace - wsl - - deadcode # deprecated - - golint # deprecated - - ifshort # deprecated - - interfacer # deprecated - - maligned # deprecated - - nosnakecase # deprecated - - scopelint # deprecated - - structcheck # deprecated - - varcheck # deprecated + - tenv # deprecated + fast: false linters-settings: @@ -45,6 +39,10 @@ linters-settings: wrapcheck: ignorePackageGlobs: - github.com/switchupcb/copygen/cli/generator/* + gocritic: + settings: + ifElseChain: + minThreshold: 3 revive: rules: - name: unused-parameter diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b53461..0287a1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,9 @@ ## Contributor License Agreement -Contributions to this project must be accompanied by a **Contributor License Agreement**. You or your employer retain the copyright to your contribution. This simply gives us permission to use and redistribute your contributions as part of the project. +Contributions to this project must be accompanied by a **Contributor License Agreement**. + +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. ## Pull Requests @@ -10,7 +12,7 @@ Pull requests must pass all [CI/CD](#cicd) measures and follow the [code specifi ## Domain -The domain of Copygen lies in field manipulation. The program uses provided types to determine the fields we must assign. In this context, a `Type` refers to _the types used in a function (as parameters or results)_ rather than a type used to define variables. +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`). ## Project Structure @@ -36,17 +38,21 @@ _Read [program](examples/program/README.md) for an overview of the application's A `setup` file's abstract syntax tree is traversed once, but involves four processes. -#### Keep +#### 1. Keep + +The `setup` file is parsed using an Abstract Syntax Tree. -The `setup` file is parsed using an Abstract Syntax Tree. This tree contains the `type Copygen Interface` but also code that must be **kept** in the generated `output` file. 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`). +This tree contains the `type Copygen Interface` but also code that must be **kept** in the generated `output` file. -#### Options +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`). -**Convert** options are defined **outside** of the `type Copygen Interface` and may apply to multiple functions. As a result, 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. +#### 2. Options -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)_. As a result, 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. +**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. -#### Copygen Interface +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. + +#### 3. Copygen Interface The `type Copygen interface` is parsed to setup the `models.Function` and `models.Field` objects used in the `Matcher` and `Generator`. - [go/types Contents (Types, A -> B)](https://go.googlesource.com/example/+/HEAD/gotypes#contents) @@ -54,7 +60,7 @@ The `type Copygen interface` is parsed to setup the `models.Function` and `model - [go/types Func (Signature)](https://pkg.go.dev/go/types#Func) - [go/types Types](https://pkg.go.dev/go/types#pkg-types) -#### Imports +#### 4. Imports 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. @@ -64,28 +70,36 @@ Copygen supports three methods of generation for end-users _(developers)_: `.go` #### .go -`.go` code generation allows users to generate code using the programming language they are familiar with. `.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_. In order to do this, we must use an **interpreter**. Templates are interpreted by our [temporary yaegi fork](https://github.com/switchupcb/yaegi). `models` objects are extracted via reflection and loaded into the interpreter. Then, the interpreter interprets the provided `.go` template file _(specified by the user)_ to run the `Generate()` function. +`.go` code generation allows users to generate code using the programming language they are familiar with. + +`.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. + +`.go` templates are interpreted by a [yaegi fork](https://github.com/switchupcb/yaegi). +1. `models` objects are extracted via reflection and loaded into the interpreter. +2. Then, the interpreter interprets the provided `.go` template file _(specified by the user)_ to run the `Generate()` function. #### .tmpl -`.tmpl` code generation allows users to generate code using [`text/templates`](https://pkg.go.dev/text/template). `.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_. +`.tmpl` code generation allows users to generate code using [`text/templates`](https://pkg.go.dev/text/template). + +`.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_. #### programmatic -`programmatic` code generation allows users to generate code by using `copygen` as a third-party module. For more information, read the [program example README](/examples/program/README.md). +`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). ## Specification ### From vs. To -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. As a result, **"From" comes before "To" when parsing** while **"To" comes before "From" in comparison**. +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**. ### Variable Names -| Variable | Description | -| :------- | :----------------------------------------------------------------------------------- | -| from.* | Variables preceded by from indicate from-functionality. | -| to.* | Variables preceded by to indicate to-functionality. | +| Variable | Description | +| :------- | :------------------------------------------------------ | +| from.* | Variables preceded by from indicate from-functionality. | +| to.* | Variables preceded by to indicate to-functionality. | ### Comments @@ -93,14 +107,14 @@ Comments follow [Effective Go](https://golang.org/doc/effective_go#commentary) a ### Why Pointers -Contrary to the README, pointers aren't used — on Fields — as a performance optimization. Using pointers with Fields makes it less likely for a mistake to occur during the comparison of them. For example, using a for-copy loop on a `[]models.Field`: +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`: ```go -// A copy of field is created with a separate pointer. +// A copy of field is created with a distinct memory address. for _, field := range fields { - // fromField.To still points to the original field. - // fromField.From points to a field which is NOT the copied field (but has the same values). - if field == fromField.To { + // field.To still points to the original field's .To memory address. + // 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. + if field == field.To.From { // never happens ... } @@ -109,22 +123,24 @@ for _, field := range fields { ### Anti-patterns -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`. As a result, `models.Field` objects are pointed directly to maintain simplicity. +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. -Using the `*models.Field` definition for a `models.Field`'s `From` and `To` fields can be placed into a `type FieldRelation`: `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. +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. ## CI/CD ### Static Code Analysis -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.52.1` 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. +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. If you receive `File is not ... with -...`, use `golangci-lint run --disable-all --no-config -Egofmt --fix`. #### Fieldalignment -**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)_. As a result, 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 -). 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`. +**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 +). + +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`. **ALWAYS COMMIT BEFORE USING `fieldalignment -fix ./cli/...`** as it may remove comments. @@ -134,7 +150,6 @@ For information on testing, read [Tests](examples/_tests/). # Roadmap -Focus on these features: - - Options, Matcher, Generator: `cast` option for [direct type conversions](https://go.dev/ref/spec#Conversions) _(as opposed to a convert function)_ +Implement the following features. - Generator: deepcopy - Parser: Fix Free-floating comments _(add structs in [`multi`](examples/_tests/multi/copygen.go) to test)_ diff --git a/LICENSE b/LICENSE index e72bfdd..0ad25db 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. \ No newline at end of file diff --git a/NOTICE.md b/NOTICE.md index 59daba2..ff73a11 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,14 +1,16 @@ Copyright (C) 2022 SwitchUpCB +This file is part of Copygen. + This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by +it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. +GNU Affero General Public License for more details. -You should have received a copy of the GNU General Public License -along with this program. If not, see . \ No newline at end of file +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/README.md b/README.md index e97a6cb..8120309 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,25 @@ [![License](https://img.shields.io/github/license/switchupcb/copygen.svg?style=for-the-badge)](https://github.com/switchupcb/copygen/blob/main/LICENSE) [Mentioned in Awesome Go](https://github.com/avelino/awesome-go#generators) -Copygen is a command-line **code generator** that generates type-to-type and field-to-field code without adding any reflection or dependencies to your project. Manual-copy code generated by Copygen is [**391x faster**](https://github.com/gotidy/copy#benchmark) than **jinzhu/copier**, and adds no allocation to your program. Copygen is the most customizable type-based generator to-date and features a rich yet simple setup. Copygen supports _**every**_ Go type including `basic`, `array`, `slice`, `map`, `chan`, `interface`, and `func` types. +Copygen saves you from losing time writing repetitive code with a type-based code generator. -| Topic | Categories | -| :------------------------------ | :--------------------------------------------------------------------------------- | -| [Usage](#Usage) | [Types](#types), [Setup](#setup), [Command Line](#command-line), [Output](#output) | -| [Customization](#customization) | [Custom Objects](#custom-objects), [Templates](#templates) | -| [Matcher](#matcher) | [Automatch](#automatch), [Manual](#manual), [Depth](#depth) | -| [Usecases](#usecase) | [When to Use](#when-to-use-copygen), [Custom Generation](#custom-generation) | -| [License](#license) | [What can I do?](#what-can-i-do), [License Exception](#license-exception) | +## What is Copygen? -## Usage +Copygen is a command-line and programmatic **code generator** that generates custom type-based code, including type-to-type and field-to-field code without adding any reflection or dependencies to your project. Manual-copy code generated by Copygen is [**391x faster**](https://github.com/gotidy/copy#benchmark) than **jinzhu/copier** and adds no allocation to your program. + +_Copygen supports **every** Go type including `basic`, `array`, `slice`, `map`, `chan`, `interface`, and `func` types._ + +## Table of Contents + +| Topic | Categories | +| :------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| [Usage](#how-do-you-use-copygen) | [Types](#step-1-define-go-types), [Setup](#step-2-configure-the-setup-files), [Command Line](#step-3-use-the-command-line), [Output](#output) | +| [Customization](#customization) | [Custom Objects](#custom-objects), [Templates](#templates) | +| [Matcher](#matcher) | [Automatch](#automatch), [Manual](#manual), [Depth](#depth) | +| [Usecases](#usecase) | [When to Use](#when-to-use-copygen), [Custom Generation](#custom-generation) | +| [License](#what-is-the-license) | [What can I do?](#what-can-you-do-with-this-license), [License Exception](#what-is-a-license-exception) | + +## How do you use Copygen? Each example has a **README**. @@ -26,9 +34,10 @@ Each example has a **README**. | :------------------------------- | :--------------------------------------- | | main | The default example. | | [basic](examples/basic/) | Matches a `basic` type to a field. | -| [automatch](examples/automatch/) | Uses the automatch feature with depth. | -| [map](examples/map/) | Uses the manual map feature. | -| [tag](examples/tag/) | Uses the manual tag feature. | +| [automatch](examples/automatch/) | Uses the automatch matcher with depth. | +| [map](examples/map/) | Uses the manual map matcher. | +| [tag](examples/tag/) | Uses the manual tag matcher. | +| [cast](examples/cast/) | Uses the cast modifier. | | deepcopy _(Roadmap)_ | Uses the deepcopy option. | | [error](examples/error/) | Uses `.go` templates to return an error. | | [tmpl](examples/tmpl/) | Uses `.tmpl` templates. | @@ -38,9 +47,9 @@ _*[`multi`](examples/_tests/multi/setup/setup.go) tests every type._ This [example](examples/main/) uses three type-structs to generate the `ModelsToDomain()` function using a Command Line Interface. -### Types +### Step 1. Define Go Types -The following types are defined in a program. +Go types are defined in a file. `./domain/domain.go` @@ -81,9 +90,9 @@ type User struct { The `models.Account` and `models.User` fields will be copied to `domain.Account` fields. -### Setup +### Step 2. Configure the setup files. -Setting up Copygen is a 2-step process involving a `YML` and `GO` file. +You set up Copygen with a `YML` and `GO` file. #### setup.yml @@ -94,50 +103,58 @@ generated: output: ../copygen.go # Define the optional custom templates used to generate the file (.go, .tmpl supported). - template: ./generate.go + # template: ./generate.go # Define custom options (which are passed to generator options) for customization. custom: option: The possibilities are endless. ``` -_The main example ignores the template fields._ - #### setup.go -Create an interface in the specified setup file with a `type Copygen interface`. In each function, specify _the types you want to copy from_ as parameters, and _the type you want to copy to_ as return values. _This interface is inspired by **goverter**._ +Define a `type Copygen interface` in the specified setup file. + +In each function, specify _the types you want to copy from_ as parameters, and _the types you want to copy to_ as return values. + +_This interface is inspired by **goverter**._ ```go /* Specify the name of the generated file's package. */ package copygen -/* Copygen defines the functions that will be generated. */ +/* Copygen defines the functions that are generated. */ type Copygen interface { // custom see table below for options ModelsToDomain(*models.Account, *models.User) *domain.Account } ``` -_Copygen uses no allocation **with pointers** because Go is pass-by-value. Specifying a pointer results in the object's fields being assigned directly; as opposed to a copy of the object's fields._ +_Copygen uses no allocation **with pointers** because Go is pass-by-value. So, using a pointer results in the object's fields being assigned directly as opposed to a copy of the object's fields._ #### options -You can specify options for your Copygen functions using comments: Do **NOT** put empty lines between comments that pertain to one function. **Options are evaluated in order of declaration.** +Use comments to specify options for Copygen functions: Do **NOT** add empty lines between comments that pertain to one function. -| Option | Use | Description | Example | -| :------------------ | :------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | -| `map from to` | Map fields manually. | Copygen uses its [automatcher](#automatch) by default.
Override this to `map` fields to and from each other.
Regex is supported for from-fields. | `map .* package.Type.Field`
`map models.Account.ID domain.Account.ID` | -| `tag field key` | Map fields manually using tags. | Copygen uses its [automatcher](#automatch) by default.
Override this using `tag` with _regex_ and a tag key. | `tag package.Type.Field key`
`tag .* api` _(all fields)_ | -| `depth field level` | Use a specific field depth. | Copygen uses full-field [depth](#depth) by default.
Override this using `depth` with _regex_ and a [depth-level](#depth) integer. | `depth .* 2`
`depth models.Account.* 1` | -| `deepcopy field` | Deepcopy from-fields. | Copygen shallow copies fields by default.
Override this using `deepcopy` with _regex_.
For more info, view [Shallow Copy vs. Deep Copy](#shallow-copy-vs-deep-copy). | `deepcopy package.Type.Field`
`deepcopy .*` _(all fields)_ | -| `automatch field` | Use the automatcher selectively or with `map` and `tag`. | Using `map` or `tag` disables the default automatcher.
Enable it using `automatch` with _regex_.
| `automatch package.Type.Field`
`automatch models.User.*` | -| `custom option` | Specify custom function options. | Use custom options with [templates](#templates).
Returns `map[string][]string` _(trim-spaced)_. | `ignore true`
`swap false` | +**Options are evaluated in order of declaration.** + +| Option | Use | Description | Example | +| :------------------ | :--------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | +| `automatch field` | Use the automatcher selectively or with `map` and `tag` options. | Using `map` or `tag` disables the default automatcher.
Enable it again using `automatch` with _regex_. | `automatch package.Type.Field`
`automatch models.User.*` | +| `map from to` | Map fields manually. | `map` fields to and from each other.
Regex is supported for from-fields. | `map .* package.Type.Field`
`map models.Account.ID domain.Account.ID` | +| `tag field key` | Map fields manually using struct tags. | Use `tag` with _regex_ and a tag key. | `tag package.Type.Field key`
`tag .* api` _(all fields)_ | +| `depth field level` | Use a specific field depth. | Copygen uses full-field [depth](#depth) by default.
Override this using `depth` with _regex_ and a [depth-level](#depth) integer. | `depth .* 2`
`depth models.Account.* 1` | +| `deepcopy field` | Deepcopy from-fields. | Copygen shallow copies fields by default.
Override this using `deepcopy` with _regex_.
For more info, view [Shallow Copy vs. Deep Copy](#shallow-copy-vs-deep-copy). | `deepcopy package.Type.Field`
`deepcopy .*` _(all fields)_ | +| `custom option` | Specify custom function options. | Use custom options with [templates](#templates).
Returns `map[string][]string` _(trim-spaced)_. | `ignore true`
`swap false` | _[View a reference on Regex.](https://cheatography.com/davechild/cheat-sheets/regular-expressions/)_ +A matching option _(e.g. `map`, `automatch`, `tag`)_ determines whether the field will be matched to another field, but a modifying option _(e.g. `convert`, `cast`)_ is only applied when a field is matched. + #### Convert -In certain cases, you may want to specify a how a specific type or field is copied with a function. This can be done by defining a function with a `convert` option. +Use the `convert function field` option to control how a type or field is copied within a function when the field is matched. + + ```go /* Define the function and field this converter is applied to using regex. */ // convert .* models.User.UserID @@ -147,9 +164,19 @@ func Itoa(i int) string { } ``` -### Command Line +_This example converts the `models.User.UserID` value using `Itoa` within all functions (`.*`) when the `models.User.UserID` field is matched._ + +#### Cast + +Use the `setup.yml` `matcher: cast` generator option to enable automatic casting when a field is matched. -Install the command line utility: Copygen is an executable and not a dependency, so use `go install`. +Use the `setup.go` `cast from to modifier` option to perform direct type assertion, conversion, expressions, function usage, and property usage with a matched field. + +For more information, read the [`cast` example](/examples/cast/). + +### Step 3. Use the Command Line + +Install the command line utility: Copygen is an executable, not a dependency, so use `go install`. ``` go install github.com/switchupcb/copygen@latest @@ -172,7 +199,7 @@ Run the executable with given options. copygen -yml path/to/yml ``` -_The path to the YML file is specified in reference to the current working directory._ +_The path to the YML file must be specified in reference to the current working directory._ ### Output @@ -208,11 +235,11 @@ func ModelsToDomain(tA *domain.Account, fA *models.Account, fU *models.User) { ## Customization -Copygen's method of input and output allows you to generate code not limited to copying fields. +Copygen's method of input and output lets you generate code that isn't limited to copying fields. ### Custom Objects -Custom types external to your application can be created for use in the `setup.go` file. When a file is generated, all types _(structs, interfaces, funcs)_ are copied **EXCEPT** the `type Copygen interface`. +Custom types external to your application can be defined in the setup file (`.go`): When an output file is generated, all types _(structs, interfaces, funcs)_ defined in the setup file (`.go`) are copied **EXCEPT** the `type Copygen interface`. ```go type DataTransferObject struct { @@ -230,15 +257,24 @@ func New() { ### Shallow Copy vs. Deep Copy -The library generates a [shallow copy](https://en.m.wikipedia.org/wiki/Object_copying#Shallow_copy) function by default. An easy way to deep-copy fields with the same return type is by using `new()` as or in a converter function **or** by using a custom template. +The library generates [shallow copy](https://en.m.wikipedia.org/wiki/Object_copying#Shallow_copy) functions by default. + +Do you need to deepcopy instead? Use `new()` within a `convert` or `cast` function **or** use a customized generator template. ### Templates -Copygen supports three methods of code generation: `.go`, `.tmpl`, and `programmatic`. View the [models.Generator](cli/models/generator.go) type for context on the parameters passed to each function. Generator options are parsed from the YML configuration file. Function options are parsed from `custom` options. Any other option represents a `FieldOption`. +Copygen supports three methods of code generation: `.go`, `.tmpl`, and `programmatic`. + +You can view the [models.Generator](cli/models/generator.go) type for context on the parameters passed to each function. +- Generator options are parsed from the YML configuration file. +- Function options are parsed from `custom` options. +- Any other option represents a `FieldOption`. #### .go -Use `.go` files to customize the code generation algorithm. The `copygen` generator uses the [`package template Generate(*models.Generator) (string, error)`](cli/generator/template/generate.go) to generate code. As a result, **this function is required** for your `.go` templates to work. The [error example](examples/error/) modifies the `.yml` in order to use **custom `.go` template functions** that `return error`. The [`template/generate.go`](/cli//generator/template/generate.go) file provides the default code generation algorithm for generating code. +Use `.go` files to customize the code generation algorithm: The `copygen` generator uses the [`package template Generate(*models.Generator) (string, error)`](cli/generator/template/generate.go) to generate code. So, **this function is required** for your `.go` templates to work. + +The [error example](examples/error/) modifies the configuration file (`.yml`) to use **custom `.go` template functions** that `return error`. The [`template/generate.go`](/cli//generator/template/generate.go) file provides the default code generation algorithm for generating code. _Use of non-extracted Go Module Imports in [`generate.go` template files](cli/generator/template/generate.go) are unsupported at the current time._ @@ -254,22 +290,27 @@ Use `copygen` as a third-party module in your application. For more information, Copygen provides two methods of field-matching: `automatch` and `manual`. -_Disable the matcher from the command-line using `-xm` or programmatically by setting `Enviroment.DisableMatcher` = true._ +_You can disable the matcher using the `matcher: skip: true` option in the setup file._ ### Automatch -When fields aren't specified using [options](#options), Copygen will attempt to automatch type-fields by name and definition. Automatch will match one from-field to many to-fields. **Automatch supports field-depth** (where types are located within fields) **and recursive types** (where the same type is in another type). Automatch loads types from Go modules _(in GOPATH)_: Ensure your modules are up to date by using `go get -u `. +Copygen automatically matches the function's fields by field name and definition when a [matching option _(`automatch`,`map`,`tag`)_](#options) isn't specified on a function. +- Automatch matches one from-field to many to-fields +- **Automatch supports field-depth** (when fields contain fields) **and recursive types** (when the field contains itself). +- Automatch loads types from Go modules _(in the `GOPATH`)_: Confirm your Go modules are up-to-date using `go get -u `. ### Manual -Using the `map` or `tag` option disables the automatcher, which allows you to manually match fields. In order to re-enable the automatcher, use the `automatch` option. Options are evaluated in order of declaration, so using `automatch .*` **after** declaring `map` and `tag` options provides an easy way to re-enable the _automatcher_ for remaining fields. +Using the `map` or `tag` option disables the automatcher, which lets you manually match fields. In order to re-enable the automatcher, use the `automatch` option. + +Options are evaluated in order of declaration, so using `automatch .*` **after** declaring `map` and `tag` options is an easy way to re-enable the _automatcher_ for remaining fields. #### Depth -The automatcher uses a field-based depth system. A field with a depth-level of 0 will only match itself. Increasing the depth-level allows its sub-fields to be matched. This system allows you to specify the depth-level for whole types **and** specific fields. +The automatcher uses a field-based depth system where a field with a depth-level of 0 only matches itself. This system lets you specify the depth-level for specific types and fields. Increasing the depth-level lets the field's sub-fields at a specified depth-level be matched. ```go -// depth-level in relation to the first-level field (0). +// Depth-level in relation to the first-level field (0). type Account // 1 ID int @@ -299,17 +340,30 @@ type Account ## Usecase -#### When to Use Copygen +### When to Use Copygen -Copygen's main purpose is to save you time by generating boilerplate code to map objects together. +Copygen's default purpose is to save you time by generating code to map objects together. -#### Why would I do that? +### Why would you do that? -In order to keep a program adaptable _(to new features)_, a program may contain two types of models. The first type of model is the **domain model** which is **used throughout your application** to model its business logic. For example, the [domain models of Copygen](cli/models/) focus on field relations and manipulation. In contrast, the ideal way to store your data _(such as in a database)_ may not match your domain model. In order to amend this problem, you create a **data model**. The [data models of Copygen](cli/config/models.go) are located in its configuration loader. In many cases, you will need a way to map these models together to exchange information from your data-model to your domain _(and vice-versa)_. It's tedious to repeatedly do this in the application _(through assignment or function definitions)_. Copygen solves this problem. +You generate code to map objects together when your program contains multiple models types (e.g., `domain`, `database`), which improves feature development speed without affecting performance. -#### Custom Generation +_Here is an example._ -Copygen's customizability with templates allows you to generate any code **based on types** _(and their respective fields, tags, etc)_. +Suppose a program maintains two types of models for feature development. +- The **domain model** is a human-optimized definition of the business logic, and is used by a human to develop business logic. For example, an `account` model which is defined for use by a human. +- The **data model** is a machine-optimized definition of the business logic, and is used by a machine to execute business logic efficiently. For example, a database model of an `account` which defines `username`, `profile_picture`, and other account related data in separate models for database efficiency. + +These models are created because the ideal method of data storage is not the ideal domain model. However, you must map these models together to exchange information from your data model to your domain model and vice-versa. + +_Here is an example of how Copygen uses this development pattern._ +- The [domain models of Copygen](cli/models/) focus on field relations and manipulation as understood by a human. +- The [data models of Copygen](cli/config/models.go) are located in its configuration loader and Go type `ast` structs. +- The "business logic" of Copygen is defined in the `parser`, `matcher`, and `generator`, which use the `domain` models of Copygen to generate code instead of underlying `ast` data structs. + +### Custom Generation + +Copygen's customizability with templates lets you generate any code **based on types** _(and their respective fields, tags, etc)_. | Example | Description | | :------ | :-------------------------------------------------------------------------------------- | @@ -318,17 +372,23 @@ Copygen's customizability with templates allows you to generate any code **based _Check out more usecases in real-world examples using [Copygen Usecases](https://github.com/switchupcb/copygen/discussions/categories/usecases)._ -## License +## What is the License? + +Copygen uses a [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.en.html). + +An exception is provided for template and example files, which are licensed under the [MIT License](cli/generator/template/LICENSE.md). + +### What can you do with this license? -Copygen uses a [GPLv3 License](https://www.gnu.org/licenses/gpl-3.0.en.html). An exception is provided for template and example files, which are licensed under the [MIT License](cli/generator/template/LICENSE.md). +**Code generated by Copygen can be used without restriction (including proprietary and commercial usage)** since generated code is not considered a derivative work. However, **modifications** to the _Copygen Software Source Code_ or implementing Copygen in a larger work **programmatically** requires you to [adhere to the AGPLv3 License](https://www.gnu.org/licenses/gpl-faq.html). -### What can I do? +_These restrictions do **NOT** apply to template or example files, as long as those files don't generate Copygen itself._ -**Code generated by Copygen can be used without restriction (including proprietary and commercial usage)** since generated code is not considered a derivative work. In contrast, **modifications** to the _Copygen Software Source Code_ and/or implementing Copygen in a larger work **programmatically** requires you to [adhere to the GPLv3 License](https://www.gnu.org/licenses/gpl-faq.html). These restrictions do **NOT** apply to template or example files, as long as those files don't generate Copygen itself. +### What is a license exception? -### License Exception +A license exception lets you modify and use Copygen programmatically **without restriction**. -A license exception allows you to modify and/or use Copygen programmatically **without restriction**. In order to purchase a license exception, please contact SwitchUpCB using the [Copygen License Exception Inquiry Form](https://switchupcb.com/copygen-license-exception/). +You can receive a license exception for Copygen by contacting SwitchUpCB using the [Copygen License Exception Inquiry Form](https://switchupcb.com/copygen-license-exception/). ## Contributing diff --git a/cli/cli.go b/cli/cli.go index 26e7976..e5dfddf 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "flag" "fmt" "os" @@ -14,10 +15,9 @@ import ( // Environment represents the copygen environment. type Environment struct { - YMLPath string // The .yml file path used as a configuration file. - Output bool // Whether to print the generated code to stdout. - Write bool // Whether to write the generated code to a file. - DisableMatcher bool // Whether to disable the matcher. + YMLPath string // The .yml file path used as a configuration file. + Output bool // Whether to print the generated code to stdout. + Write bool // Whether to write the generated code to a file. } // CLI runs copygen from a Command Line Interface and returns the exit status. @@ -41,22 +41,20 @@ func CLI() int { func (e *Environment) parseArgs() error { // define the command line arguments. var ( - ymlpath = flag.String("yml", "", "The path to the .yml flag used for code generation (from the current working directory).") - output = flag.Bool("o", false, "Use -o to print generated code to the screen.") - disablematcher = flag.Bool("xm", false, "Use -xm to disable the matcher.") + ymlpath = flag.String("yml", "", "The path to the .yml flag used for code generation (from the current working directory).") + output = flag.Bool("o", false, "Use -o to print generated code to the screen.") ) // parse the command line arguments. flag.Parse() if !strings.HasSuffix(*ymlpath, ".yml") { - return fmt.Errorf("you must specify a .yml configuration file using -yml") + return errors.New("you must specify a .yml configuration file using -yml") } e.YMLPath = *ymlpath e.Output = *output e.Write = true - e.DisableMatcher = *disablematcher return nil } @@ -75,7 +73,7 @@ func (e *Environment) Run() (string, error) { } // The matcher is run on the parsed data (to create the objects used during generation). - if !e.DisableMatcher { + if !gen.Options.Matcher.Skip { if err = matcher.Match(gen); err != nil { return "", fmt.Errorf("%w", err) } diff --git a/cli/config/models.go b/cli/config/models.go index 498ef6e..dae8e4d 100644 --- a/cli/config/models.go +++ b/cli/config/models.go @@ -5,6 +5,7 @@ package config type YML struct { Options map[string]interface{} `yaml:"custom"` Generated Generated `yaml:"generated"` + Matcher Matcher `yaml:"matcher"` } // Generated represents generated properties of the YML file. @@ -13,3 +14,23 @@ type Generated struct { Output string `yaml:"output"` Template string `yaml:"template"` } + +// Matcher represents matcher properties of the YML file. +type Matcher struct { + Skip bool `yaml:"skip"` + Cast Cast `yaml:"cast"` +} + +// Cast represents matcher cast properties of the YML file. +type Cast struct { + Depth int `yaml:"depth"` + Enabled bool `yaml:"enabled"` + Disabled Disabled `yaml:"disabled"` +} + +// Disabled represents matcher cast feature flags of the YML file. +type Disabled struct { + AssignObjectInterface bool `yaml:"assignObjectInterface"` + AssertInterfaceObject bool `yaml:"assertInterfaceObject"` + Convert bool `yaml:"convert"` +} diff --git a/cli/config/yml.go b/cli/config/yml.go index 6dc8a65..b5d2b8a 100644 --- a/cli/config/yml.go +++ b/cli/config/yml.go @@ -51,6 +51,14 @@ func ParseYML(yml YML) *models.Generator { Outpath: yml.Generated.Output, Tempath: yml.Generated.Template, Options: models.GeneratorOptions{ + Matcher: models.MatcherOptions{ + Skip: yml.Matcher.Skip, + AutoCast: yml.Matcher.Cast.Enabled, + CastDepth: yml.Matcher.Cast.Depth, + DisableAssignObjectInterface: yml.Matcher.Cast.Disabled.AssignObjectInterface, + DisableAssertInterfaceObject: yml.Matcher.Cast.Disabled.AssertInterfaceObject, + DisableConvert: yml.Matcher.Cast.Disabled.Convert, + }, Custom: yml.Options, }, } diff --git a/cli/generator/generator.go b/cli/generator/generator.go index 00c73bc..af13b07 100644 --- a/cli/generator/generator.go +++ b/cli/generator/generator.go @@ -3,6 +3,7 @@ package generator import ( "bytes" + "errors" "fmt" "go/format" "os" @@ -99,7 +100,7 @@ func GenerateCode(gen *models.Generator) (string, error) { fn, ok := v.Interface().(func(*models.Generator) (string, error)) if !ok { - return "", fmt.Errorf("the template function `Generate` could not be type asserted. Is it a func(*models.Generator) (string, error)?") + return "", errors.New("the template function `Generate` could not be type asserted. Is it a func(*models.Generator) (string, error)?") } content, err := fn(gen) diff --git a/cli/generator/template/LICENSE.md b/cli/generator/template/LICENSE.md index 2c96c47..1e505b9 100644 --- a/cli/generator/template/LICENSE.md +++ b/cli/generator/template/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 - 2022 SwitchUpCB +Copyright (c) 2021 - 2025 SwitchUpCB Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/cli/generator/template/generate.go b/cli/generator/template/generate.go index f50ac9d..5315496 100644 --- a/cli/generator/template/generate.go +++ b/cli/generator/template/generate.go @@ -110,6 +110,8 @@ func generateAssignment(toType models.Type) string { fromField := toField.From if fromField.Options.Convert != "" { assign.WriteString(fromField.Options.Convert + "(" + fromField.FullVariableName("") + ")\n") + } else if fromField.Options.Cast != "" { + assign.WriteString(fromField.FullVariableName("") + "." + fromField.Options.Cast + "\n") } else { switch { case toField.FullDefinition() == fromField.FullDefinition(): diff --git a/cli/models/field.go b/cli/models/field.go index 6c9831b..5fe8a5a 100644 --- a/cli/models/field.go +++ b/cli/models/field.go @@ -62,6 +62,9 @@ type Field struct { // FieldOptions represent options for a Field. type FieldOptions struct { + // The function the field is casted with. + Cast string + // The function the field is converted with (as a parameter). Convert string @@ -91,6 +94,7 @@ func (f *Field) Deepcopy(cyclic map[*Field]bool) *Field { Definition: f.Definition, Underlying: f.Underlying, Options: FieldOptions{ + Cast: f.Options.Cast, Convert: f.Options.Convert, Map: f.Options.Map, Tag: f.Options.Tag, diff --git a/cli/models/generator.go b/cli/models/generator.go index c8792b4..92fdeae 100644 --- a/cli/models/generator.go +++ b/cli/models/generator.go @@ -10,7 +10,18 @@ type Generator struct { Keep []byte // The code that is kept from the setup file. } -// GeneratorOptions represent options for a Generator. +// GeneratorOptions represents options for a Generator. type GeneratorOptions struct { - Custom map[string]interface{} // The custom options of a generator. + Custom map[string]interface{} // The custom options of a generator. + Matcher MatcherOptions // The options for the matcher of a generator. +} + +// MatcherOptions represents options for the Generator's matcher. +type MatcherOptions struct { + CastDepth int // The option that sets the maximum depth for automatic casting. + Skip bool // The option that skips the matcher. + AutoCast bool // The option that enables automatic casting. + DisableAssignObjectInterface bool // The cast option feature flag that disables assignment of objects to interfaces. + DisableAssertInterfaceObject bool // The cast option feature flag that disables assignment of interfaces to objects. + DisableConvert bool // The cast option feature flag that disables type conversion. } diff --git a/cli/parser/field.go b/cli/parser/field.go index 27764d1..25509cb 100644 --- a/cli/parser/field.go +++ b/cli/parser/field.go @@ -3,6 +3,7 @@ package parser import ( "fmt" "go/types" + "strconv" "strings" "github.com/fatih/structtag" @@ -63,7 +64,7 @@ func parseField(typ types.Type) *models.Field { field.VariableName = "." + alphastring(elemfield.Definition) case *types.Array: - field.Definition = "[" + fmt.Sprint(x.Len()) + "]" + collectedDefinition(parseField(x.Elem())) + field.Definition = "[" + strconv.FormatInt(x.Len(), 10) + "]" + collectedDefinition(parseField(x.Elem())) case *types.Slice: field.Definition = models.CollectionSlice + collectedDefinition(parseField(x.Elem())) diff --git a/cli/parser/function.go b/cli/parser/function.go index 2a5c6e8..e88d99c 100644 --- a/cli/parser/function.go +++ b/cli/parser/function.go @@ -1,6 +1,7 @@ package parser import ( + "errors" "fmt" "go/ast" "go/types" @@ -10,7 +11,7 @@ import ( ) // parseFunctions parses the AST for functions in the setup file. -// astcopygen is used to assign options from *ast.Comments. +// The copygen *ast.InterfaceType is used to assign options from *ast.Comments. func (p *Parser) parseFunctions(copygen *ast.InterfaceType) ([]models.Function, error) { numMethods := len(copygen.Methods.List) if numMethods == 0 { @@ -25,7 +26,13 @@ func (p *Parser) parseFunctions(copygen *ast.InterfaceType) ([]models.Function, // create models.Type objects. fieldoptions, manual := getNodeOptions(copygen.Methods.List[i], p.Options.CommentOptionMap) fieldoptions = append(fieldoptions, p.Options.ConvertOptions...) - parsed, err := parseTypes(method.(*types.Func)) + + methodFuncs, ok := method.(*types.Func) + if !ok { + return nil, errors.New("method object is not a Go types function") + } + + parsed, err := parseTypes(methodFuncs) if err != nil { return nil, fmt.Errorf("an error occurred while parsing the types of function %q.\n%w", method.Name(), err) } diff --git a/cli/parser/keep.go b/cli/parser/keep.go index 7d610d4..62b4977 100644 --- a/cli/parser/keep.go +++ b/cli/parser/keep.go @@ -1,6 +1,7 @@ package parser import ( + "errors" "fmt" "go/ast" "strings" @@ -10,7 +11,7 @@ import ( const convertOptionSplitAmount = 3 -// Keep removes ast.Nodes from an ast.File that will be kept in a generated output file. +// Keep removes ast.Nodes from an ast.File that are kept in a generated output file. func (p *Parser) Keep(astFile *ast.File) error { var foundCopygenInterface bool var trash []*ast.Comment @@ -46,11 +47,11 @@ func (p *Parser) Keep(astFile *ast.File) error { } } - // Remove ast.Comments that will be parsed into options from the ast.File. + // Remove ast.Comments that are parsed into options from the ast.File. astRemoveComments(astFile, trash) if !foundCopygenInterface { - return fmt.Errorf("the \"type Copygen interface\" could not be found (in the setup file's AST)") + return errors.New("the \"type Copygen interface\" could not be found (in the setup file's AST)") } return nil diff --git a/cli/parser/options/cast.go b/cli/parser/options/cast.go new file mode 100644 index 0000000..ead3f9e --- /dev/null +++ b/cli/parser/options/cast.go @@ -0,0 +1,60 @@ +package options + +import ( + "fmt" + "regexp" + "strings" + + "github.com/switchupcb/copygen/cli/models" +) + +const ( + CategoryCast = "cast" + + // FormatCast represents an end-user facing format for a cast option. + //