Skip to content

Commit

Permalink
Merge pull request #136 from jmattheis/enum
Browse files Browse the repository at this point in the history
Enum Support
  • Loading branch information
jmattheis authored Feb 22, 2024
2 parents e137dd1 + b014e67 commit 3b5f52f
Show file tree
Hide file tree
Showing 105 changed files with 3,368 additions and 264 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ jobs:
- run: go test ./...
env:
SKIP_VERSION_DEPENDENT: 'true'
generate:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.repository != github.event.pull_request.head.repo.full_name)
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: "1.21.x"
- uses: actions/checkout@v4
- run: mkdir covdata
- run: GOCOVERDIR="$PWD/covdata" go generate ./...
- run: go tool covdata textfmt -i=./covdata -o example-coverage.txt
- uses: codecov/codecov-action@v4
with:
disable_search: true
files: ./example-coverage.txt
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- run: git diff --exit-code
- if: failure()
run: echo "::error::Check failed, please run 'go generate ./...' and commit the changes."
build_docs:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.repository != github.event.pull_request.head.repo.full_name)
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/c.out
/coverage.txt
/.vscode
/covdata

node_modules/
/docs/.vitepress/cache
Expand Down
135 changes: 57 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ do is create an interface and execute goverter. The project is meant as
alternative to [jinzhu/copier](https://github.com/jinzhu/copier) that doesn't
use reflection.

[Getting Started](https://goverter.jmattheis.de/guide/getting-started)
[Installation](https://goverter.jmattheis.de/guide/install)
[CLI](https://goverter.jmattheis.de/reference/cli)
[Config](https://goverter.jmattheis.de/reference/settings)
Expand All @@ -36,6 +37,7 @@ use reflection.
- **Fast execution**: No reflection is used at runtime
- Automatically converts builtin types: slices, maps, named types, primitive
types, pointers, structs with same fields
- [Enum support](https://goverter.jmattheis.de/guide/enum)
- [Deep copies](https://en.wikipedia.org/wiki/Object_copying#Deep_copy) per
default and supports [shallow
copying](https://en.wikipedia.org/wiki/Object_copying#Shallow_copy)
Expand All @@ -46,87 +48,64 @@ use reflection.
- Detailed [documentation](https://goverter.jmattheis.de/) with a lot examples
- Thoroughly tested, see [our test scenarios](./scenario)

## Getting Started

1. Ensure your `go version` is 1.16 or above

1. Create a go modules project if you haven't done so already

```bash
$ go mod init module-name
```

1. Create your converter interface and mark it with a comment containing
[`goverter:converter`](https://goverter.jmattheis.de/reference/converter)

`input.go`

```go
package example
// goverter:converter
type Converter interface {
ConvertItems(source []Input) []Output
// goverter:ignore Irrelevant
// goverter:map Nested.AgeInYears Age
Convert(source Input) Output
}
type Input struct {
Name string
Nested InputNested
}
type InputNested struct {
AgeInYears int
}
type Output struct {
Name string
Age int
Irrelevant bool
}
```

See [Settings](https://goverter.jmattheis.de/reference/settings) for more information.

1. Run `goverter`:

```bash
$ go run github.com/jmattheis/goverter/cmd/goverter@latest gen ./
```

It's recommended to use an explicit version instead of `latest`. See
[Installation](https://goverter.jmattheis.de/guide/install) and
[CLI](https://goverter.jmattheis.de/reference/cli) for more information.
1. goverter created a file at `./generated/generated.go`, it may look like this:
```go
package generated
import example "goverter/example"
type ConverterImpl struct{}
func (c *ConverterImpl) Convert(source example.Input) example.Output {
var exampleOutput example.Output
exampleOutput.Name = source.Name
exampleOutput.Age = source.Nested.AgeInYears
return exampleOutput
}
func (c *ConverterImpl) ConvertItems(source []example.Input) []example.Output {
var exampleOutputList []example.Output
if source != nil {
exampleOutputList = make([]example.Output, len(source))
for i := 0; i < len(source); i++ {
exampleOutputList[i] = c.Convert(source[i])
}
## Example

Given this converter:

```go
package example

// goverter:converter
type Converter interface {
ConvertItems(source []Input) []Output

// goverter:ignore Irrelevant
// goverter:map Nested.AgeInYears Age
Convert(source Input) Output
}

type Input struct {
Name string
Nested InputNested
}
type InputNested struct {
AgeInYears int
}
type Output struct {
Name string
Age int
Irrelevant bool
}
```

Goverter will generated these conversion methods:

```go
package generated

import example "goverter/example"

type ConverterImpl struct{}

func (c *ConverterImpl) Convert(source example.Input) example.Output {
var exampleOutput example.Output
exampleOutput.Name = source.Name
exampleOutput.Age = source.Nested.AgeInYears
return exampleOutput
}
func (c *ConverterImpl) ConvertItems(source []example.Input) []example.Output {
var exampleOutputList []example.Output
if source != nil {
exampleOutputList = make([]example.Output, len(source))
for i := 0; i < len(source); i++ {
exampleOutputList[i] = c.Convert(source[i])
}
return exampleOutputList
}
```
return exampleOutputList
}
```

See [Generation](https://goverter.jmattheis.de/explanation/generation) for more information.
See [Getting Started](https://goverter.jmattheis.de/guide/getting-started).

## Versioning

Expand Down
16 changes: 16 additions & 0 deletions builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type Generator interface {
sourceID *xtype.JenID,
source, target *xtype.Type,
path ErrorPath) ([]jen.Code, *xtype.JenID, *Error)

ReturnError(ctx *MethodContext,
path ErrorPath,
id *jen.Statement) (jen.Code, bool)
}

// MethodContext exposes information for the current method.
Expand Down Expand Up @@ -100,6 +104,18 @@ func (ctx *MethodContext) DefinedFields(target *xtype.Type) map[string]struct{}
return f
}

func (ctx *MethodContext) DefinedEnumFields(target *xtype.Type) map[string]struct{} {
if ctx.FieldsTarget != target.String {
return emptyFields
}

f := map[string]struct{}{}
for name := range ctx.Conf.EnumMapping.Map {
f[name] = struct{}{}
}
return f
}

var (
emptyMapping *config.FieldMapping = &config.FieldMapping{}
emptyFields = map[string]struct{}{}
Expand Down
143 changes: 143 additions & 0 deletions builder/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package builder

import (
"fmt"

"github.com/dave/jennifer/jen"
"github.com/jmattheis/goverter/config"
"github.com/jmattheis/goverter/enum"
"github.com/jmattheis/goverter/xtype"
)

// Basic handles basic data types.
type Enum struct{}

// Matches returns true, if the builder can create handle the given types.
func (*Enum) Matches(ctx *MethodContext, source, target *xtype.Type) bool {
return ctx.Conf.Enum.Enabled &&
source.Enum(&ctx.Conf.Enum).OK &&
target.Enum(&ctx.Conf.Enum).OK
}

// Build creates conversion source code for the given source and target type.
func (*Enum) Build(gen Generator, ctx *MethodContext, sourceID *xtype.JenID, source, target *xtype.Type, path ErrorPath) ([]jen.Code, *xtype.JenID, *Error) {
stmt, nameVar, err := buildTargetVar(gen, ctx, sourceID, source, target, path)
if err != nil {
return nil, nil, err
}

var cases []jen.Code

targetEnum := target.Enum(&ctx.Conf.Enum)
sourceEnum := source.Enum(&ctx.Conf.Enum)

definedKeys := ctx.DefinedEnumFields(target)

transformerMapping, err := executeTransformers(ctx.Conf.EnumMapping.Transformers, source, target, sourceEnum, targetEnum)
if err != nil {
return nil, nil, err
}

for _, sourceName := range sourceEnum.SortedMembers() {
value := sourceEnum.Members[sourceName]
delete(definedKeys, sourceName)

targetName, ok := ctx.Conf.EnumMapping.Map[sourceName]
if !ok {
targetName, ok = transformerMapping[sourceName]
}

if !ok {
targetName = sourceName
}

sourceQual := jen.Qual(source.NamedType.Obj().Pkg().Path(), sourceName)
body, err := caseAction(gen, ctx, nameVar, target, targetEnum, targetName, sourceID, path)
if err != nil {
return nil, nil, err.Lift(&Path{
SourceType: fmt.Sprint("constant: ", value),
SourceID: sourceName,
Prefix: ".",
TargetID: targetName,
TargetType: "???",
})
}
cases = append(cases, jen.Case(sourceQual).Add(body))
}

enumUnknown := ctx.Conf.Common.Enum.Unknown
if enumUnknown == "" {
return nil, nil, NewError("No enum:unknown configured.\nSee https://goverter.jmattheis.de/guide/enum#unknown-enum-values")
}

body, err := caseAction(gen, ctx, nameVar, target, targetEnum, enumUnknown, sourceID, path)
if err != nil {
return nil, nil, err.Lift(&Path{
SourceID: "@enum:unknown",
Prefix: ".",
TargetID: enumUnknown,
TargetType: "???",
})
}
cases = append(cases, jen.Default().Add(body))

for name := range definedKeys {
return nil, nil, NewError(fmt.Sprintf("Configured enum value %s does not exist on\n %s", name, source.String)).
Lift(&Path{
Prefix: ".",
SourceID: name,
SourceType: "???",
})
}

stmt = append(stmt, jen.Switch(sourceID.Code).Block(cases...))
return stmt, xtype.VariableID(nameVar), nil
}

func caseAction(gen Generator, ctx *MethodContext, nameVar *jen.Statement, target *xtype.Type, targetEnum *xtype.Enum, targetName string, sourceID *xtype.JenID, errPath ErrorPath) (jen.Code, *Error) {
if config.IsEnumAction(targetName) {
switch targetName {
case config.EnumActionIgnore:
return jen.Comment("ignored"), nil
case config.EnumActionPanic:
return jen.Panic(jen.Qual("fmt", "Sprintf").Call(jen.Lit("unexpected enum element: %v"), sourceID.Code.Clone())), nil
case config.EnumActionError:
errStmt := jen.Qual("fmt", "Errorf").Call(jen.Lit("unexpected enum element: %v"), sourceID.Code.Clone())
code, ok := gen.ReturnError(ctx, errPath, errStmt)
if !ok {
return nil, NewError(fmt.Sprintf("Cannot return %s because the explicitly defined conversion method doesn't return an error.", config.EnumActionError))
}
return code, nil
default:
return nil, NewError(fmt.Sprintf("invalid target %q", targetName))
}
}
_, ok := targetEnum.Members[targetName]
if !ok {
return nil, NewError(fmt.Sprintf("Enum %s does not exist on\n %s", targetName, target.String))
}

targetQual := jen.Qual(target.NamedType.Obj().Pkg().Path(), targetName)
return nameVar.Clone().Op("=").Add(targetQual), nil
}

func executeTransformers(transformers []config.ConfiguredTransformer, source, target *xtype.Type, sourceEnum, targetEnum *xtype.Enum) (map[string]string, *Error) {
transformerMapping := map[string]string{}
for _, t := range transformers {
m, err := t.Transformer(enum.TransformContext{
Source: enum.Enum{Type: source.NamedType, Members: sourceEnum.Members},
Target: enum.Enum{Type: target.NamedType, Members: targetEnum.Members},
Config: t.Config,
})
if err != nil {
return nil, NewError(fmt.Sprintf("error executing transformer %q with config %q: %s", t.Name, t.Config, err))
}
if len(m) == 0 {
return nil, NewError(fmt.Sprintf("transformer %q with config %q did not return any mapped values. Is there an configuration error?", t.Name, t.Config))
}
for key, value := range m {
transformerMapping[key] = value
}
}
return transformerMapping, nil
}
2 changes: 2 additions & 0 deletions cli/cli.go → cli/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/jmattheis/goverter"
"github.com/jmattheis/goverter/config"
"github.com/jmattheis/goverter/enum"
)

type Strings []string
Expand Down Expand Up @@ -56,6 +57,7 @@ func Parse(args []string) (*goverter.GenerateConfig, error) {
BuildTags: *buildTags,
OutputBuildConstraint: *outputConstraint,
WorkingDir: *cwd,
EnumTransformers: map[string]enum.Transformer{},
Global: config.RawLines{
Lines: global,
Location: "command line (-g, -global)",
Expand Down
Loading

0 comments on commit 3b5f52f

Please sign in to comment.