simplegen

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2023 License: MIT Imports: 11 Imported by: 0

README

simplegen

Tool for simple use of ast-based code generation.

Go Reference

Problem

In large codebase or in microservice architecture could be a bunch of codegen tools on many files. There often runs via go:generate. Which leads to slowdown of build process, because each call to go:generate means that tool should parse file (slow), build ast.Tree (slow), do some stuff with ast (usually fast) and write new file (slow).

Solution

simplegen do all slow stuff only once allowing developer to use ast to collect data for codegen templates.

Usage

package main

import (
    "flag"
    "go/ast"
    "golang.org/x/tools/go/packages"

    "github.com/AlwxSin/simplegen"
)

var PaginatorTemplate = `
{{ range $key, $struct := .Specs }}
// {{$struct.Name}}ListPaginated represents {{$struct.Name}} list in a pagination container.
type {{$struct.Name}}ListPaginated struct {
    CurrentCursor *string ` + "`json:\"currentCursor\"`\n" +
    `	NextCursor    *string ` + "`json:\"nextCursor\"`\n" +
    `	Results       []*{{$struct.Name}} ` + "`json:\"results\"`\n" +
    `
    isPaginated bool
    limit       int
    offset      int
}
`

func Paginator(
    sg *simplegen.SimpleGenerator,
    pkg *packages.Package,
    node *ast.TypeSpec,
    comment *ast.Comment,
) (templateData simplegen.SpecData, imports []string, err error) {
    imports = append(imports, "strconv")

    type PaginatorTypeSpec struct {
        Name string
    }

    tmplData := &PaginatorTypeSpec{
        Name: node.Name.Name,
    }
    return simplegen.SpecData(tmplData), imports, nil
}

// We want all our structs to be paginated in same way, but don't want to repeat boilerplate code for pagination.
// Use magic comment
// simplegen:{generator_name}

// simplegen:paginator
type User struct {
	ID int
    Email     string
}

// simplegen:paginator
type Work struct {
	ID int
	UserID int
}

func main() {
    var pn simplegen.PackageNames // it's an alias for []string type, need for multiple cli arguments

    flag.Var(&pn, "package", "Package where simplegen should find magic comments")
    flag.Parse()

	// create simplegen instance with function map
	// {generator_name} -> {
	//   Template -> string with template
	//   GeneratorFunc -> func to extract data from ast.Node
    // }
    sg, _ := simplegen.NewSimpleGenerator(pn, simplegen.GeneratorsMap{
        "paginator": simplegen.TemplateGenerator{
            Template:      PaginatorTemplate,
            GeneratorFunc: Paginator,
        },
    }, nil)
    _ = sg.Generate()
}

And run it with

go run main.go -package github.com/my_project/main

-package argument is required and should be in form of {module_in_go.mod}/{path}/{package}

It can be used by go:generate as well

//go:generate go run github.com/AlwxSin/simplegen -package github.com/my_project/responses -package github.com/my_project/models
package main

func main() {
	cmd.Execute()
}
Documentation

See godoc for general API details. See examples for more ways to use simplegen.

Tools

simplegen instance has several useful methods to work with ast. For example, we have the following struct

package models

import "my_project/types"

// simplegen:my_command
type MyStruct struct {
	ID int
	Settings types.JSONB
}

And we want to generate some struct based on MyStruct fields.

// Code generated by github.com/AlwxSin/simplegen, DO NOT EDIT.
package models

import "time"
import "my_project/types"

type MyStructGenerated struct {
	// all fields from base MyStruct
	ID int
	Settings types.JSONB
	// extra fields
	CreatedAt time.Time
}

Our generator function has pkg in arguments, but it's a models package. When we parse MyStruct fields:

  1. We need StructType of MyStruct to iterate over struct fields.
  2. We should process Settings field with external JSONB type, we should know which package contains this type to import it in generated file.
package models

import (
    "strings"
    "strconv"
    "github.com/AlwxSin/simplegen"
    "golang.org/x/tools/go/packages"
    "go/ast"
    "fmt"
    "go/types"
)

func MyCommandGenerator(
    sg *simplegen.SimpleGenerator,
    pkg *packages.Package,
    node *ast.TypeSpec,
    comment *ast.Comment,
    ) (templateData simplegen.SpecData, imports []string, err error) {
	// 1. Get StructType of MyStruct
	structType, _ := sg.GetStructType(pkg, node.Name.Name)
	for i := 0; i < structType.NumFields(); i++ {
		field := structType.Field(i)
		// 2. get package of MyStruct field
		pkgPath := field.Type().(*types.Named).Obj().Pkg().Path()
		fieldPkg, _ := sg.GetPackage(pkgPath)
		fmt.Println(fieldPkg)
	}
}

Documentation

Overview

Package simplegen helps developer to generate code.

For codegen more complex than just generate small piece of code usually need to parse source code and build ast.Tree. Simplegen will take this part and give developer exact ast.Node to work with.

Also, simplegen provides tools for easy finding ast.Node.

Example:

package main

import (
	"flag"
	"fmt"
	"go/ast"
	"golang.org/x/tools/go/packages"

	"github.com/AlwxSin/simplegen"
)

var PaginatorTemplate = `
{{ range $key, $struct := .Specs }}
// {{$struct.Name}}ListPaginated represents {{$struct.Name}} list in a pagination container.
type {{$struct.Name}}ListPaginated struct {
	CurrentCursor *string ` + "`json:\"currentCursor\"`\n" +
	`	NextCursor    *string ` + "`json:\"nextCursor\"`\n" +
	`	Results       []*{{$struct.Name}} ` + "`json:\"results\"`\n" +
	`
	isPaginated bool
	limit       int
	offset      int
}
`

func Paginator(
	sg *simplegen.SimpleGenerator,
	pkg *packages.Package,
	node *ast.TypeSpec,
	comment *ast.Comment,
) (templateData simplegen.SpecData, imports []string, err error) {
	imports = append(imports, "strconv")

	type PaginatorTypeSpec struct {
		Name string
	}

	tmplData := &PaginatorTypeSpec{
		Name: node.Name.Name,
	}
	return simplegen.SpecData(tmplData), imports, nil
}

// simplegen:paginator
type User struct {
	Email     string
}

func main() {
	var pn simplegen.PackageNames

	flag.Var(&pn, "package", "Package where simplegen should find magic comments")
	flag.Parse()

	sg, err := simplegen.NewSimpleGenerator(pn, simplegen.GeneratorsMap{
		"paginator": simplegen.TemplateGenerator{
			Template:      PaginatorTemplate,
			GeneratorFunc: Paginator,
		},
	}, nil)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = sg.Generate()
	if err != nil {
		fmt.Println(err)
	}
}

In result simplegen will generate file with following content Example:

// UserListPaginated represents User list in a pagination container.
type UserListPaginated struct {
	CurrentCursor *string `json:"currentCursor"`
	NextCursor    *string `json:"nextCursor"`
	Results       []*User `json:"results"`

	isPaginated bool
	limit       int
	offset      int
}

See /examples dir for more detailed usage.

Index

Constants

View Source
const CmdKey = "simplegen"

Variables

This section is empty.

Functions

This section is empty.

Types

type GeneratorFunc

type GeneratorFunc func(
	sg *SimpleGenerator,
	pkg *packages.Package,
	node *ast.TypeSpec,
	comment *ast.Comment,
) (templateData SpecData, imports []string, err error)

GeneratorFunc for generating template data from ast nodes. SimpleGenerator calls it with: sg -> SimpleGenerator instance (for useful methods, look into docs or examples to know how to use them). pkg -> packages.Package where magic comment was found. node -> ast.TypeSpec struct annotated with magic comment. comment -> ast.Comment magic comment itself.

type GeneratorName

type GeneratorName string

type GeneratorsMap

type GeneratorsMap map[GeneratorName]TemplateGenerator

GeneratorsMap cmd_name -> func_to_generate_template_data

{
  "paginator": GeneratePaginatorData,
  "sorter": GenerateSorterData,
}

type PackageNames

type PackageNames []string

PackageNames is a helper for flag.Parse Example: flag.Var(&pn, "package", "Package where simplegen should find magic comments").

func (*PackageNames) Set

func (pn *PackageNames) Set(value string) error

func (PackageNames) String

func (pn PackageNames) String() string

type SimpleGenerator

type SimpleGenerator struct {
	// contains filtered or unexported fields
}

func NewSimpleGenerator

func NewSimpleGenerator(pkgNames PackageNames, generators GeneratorsMap, tmplFuncMap template.FuncMap) (*SimpleGenerator, error)

func (*SimpleGenerator) Generate

func (sg *SimpleGenerator) Generate() error

func (*SimpleGenerator) GetObject

func (sg *SimpleGenerator) GetObject(pkg *packages.Package, typeName string) (types.Object, error)

GetObject tries to find type object in given package. In most cases you don't need it, use GetStructType instead.

func (*SimpleGenerator) GetPackage

func (sg *SimpleGenerator) GetPackage(path string) (*packages.Package, error)

GetPackage returns packages.Package. It tries to load package if it didn't load before.

func (*SimpleGenerator) GetStructType

func (sg *SimpleGenerator) GetStructType(pkg *packages.Package, typeName string) (*types.Struct, error)

GetStructType tries to find type struct in given package.

type SpecData

type SpecData any

SpecData can be any struct. Will pass it to template.

type TemplateGenerator

type TemplateGenerator struct {
	// Template is a string which contains full template in go style
	Template      string
	GeneratorFunc GeneratorFunc
}

TemplateGenerator contains raw template and GeneratorFunc to generate template data.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL