Skip to content

Commit

Permalink
Merge pull request #9 from widmogrod/feature/name
Browse files Browse the repository at this point in the history
Version 2.0
  • Loading branch information
widmogrod authored Sep 29, 2022
2 parents 66507e7 + d9bc26b commit 6219115
Show file tree
Hide file tree
Showing 27 changed files with 1,302 additions and 102 deletions.
139 changes: 89 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,123 @@
# mkunion
Improves work with unions in golang by generating beautiful code (in other languages referred as sum types, variants, discriminators, tagged unions)

Project generates code for you, so you don't have to write it by hand.
It's a good idea to use it when you have a lot of unions in your codebase.

What is offers?
- Visitor interface with appropriate methods added to each union type
- Default implementation of Visitor that simplifies work with unions
- Reducer that can do recursive traversal & default implementation of Reducer, fantastic for traversing ASTs

Have fun! I hope you will find it useful.

## Usage
### Install mkunion
Make sure that you have installed mkunion and is in GOPATH/bin
```bash
go install github.com/widmogrod/mkunion/cmd/mkunion@1.0.1
go install github.com/widmogrod/mkunion/cmd/mkunion@2.0.0
```

Create your first union
### Create your first union
Create your first union. In our example it's a simple tree with Branch and Leaf nodes
```go
package example

//go:generate mkunion -name=Vehicle -types=Plane,Car,Boat -output=simple_union_example_gen_test.go -packageName=example
//go:generate mkunion -name=Tree -types=Branch,Leaf
type (
Car struct{}
Plane struct{}
Boat struct{}
Branch struct{ L, R Tree }
Leaf struct{ Value int }
)
```

Generated code will look like
```go
// Code generated by govisitor. DO NOT EDIT.
package example
### Generate code
Run
```
go generate ./...
```

Go will generate few files for you in the same location as union defnition
```
// source file
example/tree_example.go
// generated file
example/tree_example_mkunion_default_visitor.go
example/tree_example_mkunion_reducer.go
example/tree_example_mkunion_visitor.go
```
Don't commit generated files to your repository. They are generated on the fly.
In your CI/CD process you need to run go generate before testing & building your project.

type VehicleVisitor interface {
VisitPlane(v *Plane) any
VisitCar(v *Car) any
VisitBoat(v *Boat) any

### Use generated code
With our example you may want to sum all values in tree.

```go
tree := &Branch{
L: &Leaf{Value: 1},
R: &Branch{
L: &Leaf{Value: 2},
R: &Leaf{Value: 3},
},
}

type Vehicle interface {
Accept(g VehicleVisitor) any
var red TreeReducer[int] = &TreeDefaultReduction[int]{
OnBranch: func(x *Branch, agg int) (result int, stop bool) {
// don't do anything, but continue traversing
return agg, false
},
OnLeaf: func(x *Leaf, agg int) (int, bool) {
// add value to accumulator
return agg + x.Value, false
},
}

func (r *Plane) Accept(v VehicleVisitor) any { return v.VisitPlane(r) }
func (r *Car) Accept(v VehicleVisitor) any { return v.VisitCar(r) }
func (r *Boat) Accept(v VehicleVisitor) any { return v.VisitBoat(r) }
result := ReduceTree(red, tree, 0)
assert.Equal(t, 6, result)
```

var (
_ Vehicle = (*Plane)(nil)
_ Vehicle = (*Car)(nil)
_ Vehicle = (*Boat)(nil)
)
> Note: You can see that generated code knows how to traverse union recursively.
> You can write flat code and don't worry about recursion.
> Generator assumes that if in structure is reference to union type `Tree`, then it's recursive.
> Such code can also work on slices. You can take a look at `example/where_predicate_example.go` to see something more complex

You may decide that you want to write down your own visitor for that, then you can do it like this:
```go
var _ TreeVisitor = (*sumVisitor)(nil)

type sumVisitor struct{}

type VehicleOneOf struct {
Plane *Plane `json:",omitempty"`
Car *Car `json:",omitempty"`
Boat *Boat `json:",omitempty"`
func (s sumVisitor) VisitBranch(v *Branch) any {
return v.L.Accept(s).(int) + v.R.Accept(s).(int)
}

func (r *VehicleOneOf) Accept(v VehicleVisitor) any {
switch {
case r.Plane != nil:
return v.VisitPlane(r.Plane)
case r.Car != nil:
return v.VisitCar(r.Car)
case r.Boat != nil:
return v.VisitBoat(r.Boat)
default:
panic("unexpected")
}
func (s sumVisitor) VisitLeaf(v *Leaf) any {
return v.Value
}
```

> Note: Naturally your visitor can be more complex, but it's up to you.
var _ Vehicle = (*VehicleOneOf)(nil)
You can use `sumVisitor` like this:
```go
assert.Equal(t, 6, tree.Accept(&sumVisitor{}))
```

type mapVehicleToOneOf struct{}
## More examples
Please take a look at `./example` directory. It contains more examples of generated code.

func (t *mapVehicleToOneOf) VisitPlane(v *Plane) any { return &VehicleOneOf{Plane: v} }
func (t *mapVehicleToOneOf) VisitCar(v *Car) any { return &VehicleOneOf{Car: v} }
func (t *mapVehicleToOneOf) VisitBoat(v *Boat) any { return &VehicleOneOf{Boat: v} }
Have fun! I hope you will find it useful.

var defaultMapVehicleToOneOf VehicleVisitor = &mapVehicleToOneOf{}
## Development & contribution
When you want to contribute to this project, go for it!
Unit test are must have for any PR.

func MapVehicleToOneOf(v Vehicle) *VehicleOneOf {
return v.Accept(defaultMapVehicleToOneOf).(*VehicleOneOf)
}
```
Other than that, nothing special is required.
You may want to create issue to describe your idea before you start working on it.
That will help other developers to understand your idea and give you feedback.

## Development
```
go generate ./...
go test ./...
Expand Down
98 changes: 81 additions & 17 deletions cmd/mkunion/main.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,97 @@
package main

import (
"flag"
"context"
"github.com/urfave/cli/v2"
"github.com/widmogrod/mkunion"
"io/ioutil"
"log"
"os"
"os/signal"
"path"
"strings"
"syscall"
)

var output = flag.String("output", "-", "Output file for generated code")
var types = flag.String("types", "", "Comma separated list of golang types to generate union for")
var name = flag.String("name", "", "Name of the union type")
var packageName = flag.String("packageName", "main", "go package name")

func main() {
flag.Parse()
ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)

g := mkunion.Generate{
Types: strings.Split(*types, ","),
Name: *name,
PackageName: *packageName,
}
var app *cli.App
app = &cli.App{
Name: mkunion.Program,
Description: "VisitorGenerator union type and visitor pattern gor golang",
EnableBashCompletion: true,
DefaultCommand: "golang",
UseShortOptionHandling: true,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n", "variant"},
Required: true,
},
&cli.StringFlag{
Name: "types",
Aliases: []string{"t"},
Required: true,
},
},
Action: func(c *cli.Context) error {
cwd, _ := syscall.Getwd()
sourceName := path.Base(os.Getenv("GOFILE"))
sourcePath := path.Join(cwd, sourceName)

result, err := g.Generate()
if err != nil {
panic(err)
baseName := strings.TrimSuffix(sourceName, path.Ext(sourceName))

// file name without extension
inferred, err := mkunion.InferFromFile(sourcePath)
if err != nil {
return err
}
visitor := mkunion.VisitorGenerator{
Name: c.String("name"),
Types: strings.Split(c.String("types"), ","),
PackageName: inferred.PackageName,
}

reducer := mkunion.ReducerGenerator{
Name: visitor.Name,
Types: visitor.Types,
PackageName: inferred.PackageName,
Branches: inferred.ForVariantType(visitor.Name, visitor.Types),
}

defaultVisitor := mkunion.VisitorDefaultGenerator{
Name: visitor.Name,
Types: visitor.Types,
PackageName: inferred.PackageName,
}

generators := []struct {
gen mkunion.Generator
name string
}{
{gen: &visitor, name: "visitor"},
{gen: &reducer, name: "reducer"},
{gen: &defaultVisitor, name: "default_visitor"},
}
for _, g := range generators {
b, err := g.gen.Generate()
if err != nil {
return err
}
err = ioutil.WriteFile(path.Join(cwd,
baseName+"_"+mkunion.Program+"_"+g.name+".go"), b, 0644)
if err != nil {
return err
}
}

return nil
},
}

err = ioutil.WriteFile(*output, result, 0644)
err := app.RunContext(ctx, os.Args)
if err != nil {
panic(err)
log.Fatal(err)
}
}
8 changes: 8 additions & 0 deletions example/simple_example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package example

//go:generate go run ../cmd/mkunion/main.go -name=Vehicle -types=Plane,Car,Boat
type (
Car struct{}
Plane struct{}
Boat struct{}
)
28 changes: 28 additions & 0 deletions example/simple_example_mkunion_default_visitor.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 6219115

Please sign in to comment.