Skip to content

Commit

Permalink
implement Support for Templates
Browse files Browse the repository at this point in the history
using yaegi fork by switchupcb
  • Loading branch information
switchupcb committed Oct 19, 2021
1 parent d47d66b commit 03fe89e
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 83 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ linters:
disable:
- deadcode # The repository has no Application Life Cycle Management. Run manually prior to a release.
- unused # The repository has no Application Life Cycle Management. Run manually prior to a release.
- gomoddirectives # The repository uses go modules in its interpreter functionality from a temporary tagged fork.
- cyclop
- errorlint
- exhaustivestruct
Expand Down
9 changes: 7 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ The command-line interface _(cli)_ consists of 5 packages.

The command line interface package allows you see the flow of the program.
```go
/* Comments not present in actual project. */
// The configuration file is loaded (.yml)
gen, err := config.LoadYML(e.YMLPath)
if err != nil {
Expand All @@ -43,7 +42,7 @@ if err = parser.Parse(gen); err != nil {
return err
}

// The matcher is run on the parsed data (which are application models).
// The matcher is run on the parsed data (to create the objects used during generation).
if err = matcher.Match(gen); err != nil {
return err
}
Expand Down Expand Up @@ -99,6 +98,12 @@ for _, field := range fields {

The same reasoning applies to `for i := 0; i < count; i++` loops.

#### Antipatterns

Using the `*models.Field` definition for a `models.Field`'s `Parent` field can be considered an antipattern. 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 `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 and more room for extension.

#### CI/CD

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.42.1` and run it using `golangci-lint run`. If you recieve a `diff` error, you must add a `diff` tool in your PATH. There is one located in the `Git` bin.
Expand Down
21 changes: 9 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ Copygen is a command-line [code generator](https://github.com/gophersgang/go-cod

Each example has a **README**.

| Example | Description |
| :------------------------------------------------------------------------------ | :----------------------------------------------------------- |
| main | The default example. |
| [manual](https://github.com/switchupcb/copygen/tree/main/examples/manual) | Uses the manual map feature. |
| [automatch](https://github.com/switchupcb/copygen/tree/main/examples/automatch) | Uses the automatch feature with depth. |
| [cyclic](https://github.com/switchupcb/copygen/tree/main/examples/automatch) | Uses a cyclic type (recursive) with a shallow copy. |
| deepcopy _(Roadmap Feature)_ | Uses the deepcopy option. |
| [error](https://github.com/switchupcb/copygen/tree/main/examples/error) | Uses templates to return an error (temporarily unsupported). |
| Example | Description |
| :------------------------------------------------------------------------------ | :------------------------------------- |
| main | The default example. |
| [manual](https://github.com/switchupcb/copygen/tree/main/examples/manual) | Uses the manual map feature. |
| [automatch](https://github.com/switchupcb/copygen/tree/main/examples/automatch) | Uses the automatch feature with depth. |
| deepcopy _(Roadmap Feature)_ | Uses the deepcopy option. |
| [error](https://github.com/switchupcb/copygen/tree/main/examples/error) | Uses templates to return an error. |

This [example](https://github.com/switchupcb/copygen/blob/main/examples/main) uses three type-structs to generate the `ModelsToDomain()` function.

Expand Down Expand Up @@ -141,7 +140,7 @@ go install github.com/switchupcb/copygen@latest
Install a specific version by specifying a tag version.
```
go install github.com/switchupcb/copygen@v0.2.2
go install github.com/switchupcb/copygen@v0.2.3
```
Run the executable with given options.
Expand Down Expand Up @@ -210,9 +209,7 @@ func New() {

#### Templates

Templates can be created using **Go** to customize the generated code algorithm. The `copygen` generator uses the `package templates Generate(*models.Generator)` to generate code. As a result, this funtion is **required** for your templates to work. View [models.Generator](https://github.com/switchupcb/copygen/blob/main/cli/models/generator.go) for context on the parameters passed to each function. Generator options are parsed from the YML configuration file. Function options refer to `custom` options. Any other option represents a field option.

Templates are interpreted by [yaegi](https://github.com/traefik/yaegi) which has limitations on module imports _(Pull Request Pending)_: As a result, **templates are temporarily unsupported.** The [error example](https://github.com/switchupcb/copygen/blob/main/examples/main) modifies the .yml to use **custom functions** which `return error`. This is done by modifying the .yml and creating **custom template files**.
Templates can be created using **Go** to customize the generated code algorithm. The `copygen` generator uses the `package template Generate(*models.Generator) (string, error)` to generate code. As a result, **this function is required** for your templates to work. View the [models.Generator](https://github.com/switchupcb/copygen/blob/main/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 field option. Templates are interpreted by our [temporary yaegi fork](https://github.com/switchupcb/copygen) which supports Go modules. The [error example](https://github.com/switchupcb/copygen/blob/main/examples/main/error) modifies the `.yml` in order to use **custom template functions** that `return error`.

## Matcher

Expand Down
4 changes: 4 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,23 @@ func (e *Environment) parseArgs() error {
}

func (e *Environment) run() error {
// The configuration file is loaded (.yml)
gen, err := config.LoadYML(e.YMLPath)
if err != nil {
return err
}

// The data file is parsed (.go)
if err = parser.Parse(gen); err != nil {
return fmt.Errorf("%w", err)
}

// The matcher is run on the parsed data (to create the objects used during generation).
if err = matcher.Match(gen); err != nil {
return fmt.Errorf("%w", err)
}

// The generator is used to generate code.
if err = generator.Generate(gen, e.Output); err != nil {
return fmt.Errorf("%w", err)
}
Expand Down
37 changes: 35 additions & 2 deletions cli/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import (
"path/filepath"

"github.com/switchupcb/copygen/cli/generator/interpreter"
"github.com/switchupcb/copygen/cli/generator/template"
"github.com/switchupcb/copygen/cli/models"
)

const GenerateFunction = "template.Generate"

// Generate creates the file with generated code (with gofmt).
func Generate(gen *models.Generator, output bool) error {
// generate code
content, err := interpreter.Generate(gen)
content, err := generateCode(gen)
if err != nil {
return fmt.Errorf("an error occurred while generating code\n%v", err)
}
Expand Down Expand Up @@ -45,3 +47,34 @@ func Generate(gen *models.Generator, output bool) error {

return nil
}

// generateCode determines the func to generate function code.
func generateCode(gen *models.Generator) (string, error) {
if gen.Tempath == "" {
content, _ := template.Generate(gen)
return content, nil
}

// use an interpreted function (from a template file)
abstempath, err := filepath.Abs(filepath.Join(filepath.Dir(gen.Loadpath), gen.Tempath))
if err != nil {
return "", fmt.Errorf("an error occurred loading the absolute filepath of template path %v from the cwd %v\n%v", gen.Loadpath, gen.Tempath, err)
}

v, err := interpreter.InterpretFunction(abstempath, GenerateFunction)
if err != nil {
return "", err
}

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)?")
}

content, err := fn(gen)
if err != nil {
return "", fmt.Errorf("%w", err)
}

return content, nil
}
2 changes: 1 addition & 1 deletion cli/generator/interpreter/extract/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ package extract
import "reflect"

// Symbols are extracted from the internal types (compiled at runtime).
var Symbols map[string]map[string]reflect.Value = make(map[string]map[string]reflect.Value)
var Symbols = make(map[string]map[string]reflect.Value) // nolint:gochecknoglobals // yaegi

//go:generate yaegi extract github.com/switchupcb/copygen/cli/models
32 changes: 9 additions & 23 deletions cli/generator/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,26 @@ import (
"fmt"
"go/build"
"os"
"path"
"path/filepath"
"reflect"

"github.com/switchupcb/copygen/cli/generator/interpreter/extract"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)

// interpretFunc loads a template package.function into an interpreter.
func interpretFunc(loadpath, templatepath, symbol string) (*reflect.Value, error) {
// determine actual filepath
absfilepath, err := filepath.Abs(loadpath)
// InterpretFunction loads a template symbol from an interpreter.
func InterpretFunction(filepath, symbol string) (*reflect.Value, error) {
file, err := os.ReadFile(filepath)
if err != nil {
return nil, err
return nil, fmt.Errorf("an error occurred loading a template file: %v\nIs the relative or absoute filepath set correctly?\n%v", filepath, err)
}

absfilepath = path.Join(filepath.Dir(absfilepath), templatepath)

// read the file
file, err := os.ReadFile(absfilepath)
if err != nil {
return nil, fmt.Errorf("the specified template file for the template function %q doesn't exist: %v\nIs the relative or absoute filepath set correctly?", symbol, absfilepath)
}

source := string(file)

// setup the interpreter
goCache, err := os.UserCacheDir()
if err != nil {
return nil, fmt.Errorf("An error occurred loading the template file. Is the GOCACHE set in `go env`?\n%v", err)
return nil, fmt.Errorf("an error occurred loading the template file. Is the GOCACHE set in `go env`?\n%v", err)
}

// create the interpreter
i := interp.New(interp.Options{GoPath: os.Getenv("GOPATH"), GoCache: goCache, GoToolDir: build.ToolDir})
if err := i.Use(stdlib.Symbols); err != nil {
return nil, fmt.Errorf("an error occurred loading the template stdlib libraries\n%v", err)
Expand All @@ -46,18 +32,18 @@ func interpretFunc(loadpath, templatepath, symbol string) (*reflect.Value, error
// models.types created by the compiled binary are different from models.types created by the interpreter at runtime.
// pass the compiled models.types to the interpreter
if err := i.Use(extract.Symbols); err != nil {
return nil, fmt.Errorf("an error occurred loading the template models libary\n%v", err)
return nil, fmt.Errorf("an error occurred loading the template models library\n%v", err)
}

// load the source
if _, err := i.Eval(source); err != nil {
return nil, fmt.Errorf("an error occurred loading the template file: %v\n%v", absfilepath, err)
if _, err := i.Eval(string(file)); err != nil {
return nil, fmt.Errorf("an error occurred evaluating the template file\n%v", err)
}

// load the func from the interpreter
v, err := i.Eval(symbol)
if err != nil {
return nil, fmt.Errorf("an error occurred loading a template function\n%v", err)
return nil, fmt.Errorf("an error occurred evaluating a template function. Is it located in the file?\n%v", err)
}

return &v, nil
Expand Down
33 changes: 0 additions & 33 deletions cli/generator/interpreter/template.go

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// DO NOT CHANGE PACKAGE

// Package templates provides a template used by copygen to generate custom code.
package templates
// Package template provides a template used by copygen to generate custom code.
package template

import (
"github.com/switchupcb/copygen/cli/models"
Expand All @@ -11,14 +11,14 @@ import (
// GENERATOR FUNCTION.
// EDITABLE.
// DO NOT REMOVE.
func Generate(gen *models.Generator) string {
func Generate(gen *models.Generator) (string, error) {
content := string(gen.Keep) + "\n"

for i := range gen.Functions {
content += Function(&gen.Functions[i]) + "\n"
}

return content
return content, nil
}

// Function provides generated code for a function.
Expand Down
Loading

0 comments on commit 03fe89e

Please sign in to comment.