Skip to content

Commit

Permalink
Merge pull request #5 from Joffref/parse-attributes-and-types
Browse files Browse the repository at this point in the history
feat: parse attributes and types
  • Loading branch information
leorolland authored Nov 5, 2023
2 parents 7e84873 + b2b30d3 commit 92f7c78
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 71 deletions.
11 changes: 6 additions & 5 deletions cmd/genz/genz.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package genz
import (
"flag"
"fmt"
"github.com/Joffref/genz/internal/command"
"github.com/Joffref/genz/internal/generator"
"github.com/Joffref/genz/internal/utils"
"log"
"os"
"path/filepath"
"strings"

"github.com/Joffref/genz/internal/command"
"github.com/Joffref/genz/internal/generator"
"github.com/Joffref/genz/internal/utils"
)

type generateCommand struct {
Expand Down Expand Up @@ -72,7 +73,7 @@ func (c generateCommand) Run() error {
}

// We accept either one directory or a list of files. Which do we have?
args := flag.Args()
args := generateCmd.Args()
if len(args) == 0 {
// Default: process whole package in current directory.
args = []string{"."}
Expand All @@ -81,9 +82,9 @@ func (c generateCommand) Run() error {
// Parse the package once.
g := generator.Generator{
Template: string(template),
Pkg: utils.LoadPackage(args, tags),
}

g.ParsePackage(args, tags)
// Run generate for each type.
for _, typeName := range types {
g.Generate(typeName)
Expand Down
8 changes: 8 additions & 0 deletions examples/1_validator/main.tmpl
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
// File generated by GenZ with template validator
package test

func (v {{ .Type }}) Validate() bool {
{{ range .Attributes }}
// Validate attribute {{ .Name }} of type {{ .Type }}
{{ end }}
return true
}
6 changes: 5 additions & 1 deletion examples/1_validator/test/human.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package test

//go:generate genz -type Human -template ../main.tmpl -output human_validator.gen.go
type Human struct {

//+validator=optional
Name string
Age int

//+validator=>18,<99
Age uint
}
75 changes: 11 additions & 64 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,45 @@ package generator

import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/types"
"golang.org/x/tools/go/packages"
"log"
"strings"
"text/template"
)

// File holds a single parsed file and associated data.
type File struct {
pkg *Package // Package to which this file belongs.
file *ast.File // Parsed AST.
// These fields are reset for each type being generated.
typeName string
}

type Package struct {
name string
pkgPath string
typesInfo *types.Info
files []*File
}
"github.com/Joffref/genz/internal/parser"
"golang.org/x/tools/go/packages"
)

type Generator struct {
Template string
buf bytes.Buffer // Accumulated output.
pkg *Package // Package we are scanning.
}

// ParsePackage analyzes the single package constructed from the patterns and tags.
// ParsePackage exits if there is an error.
func (g *Generator) ParsePackage(patterns []string, tags []string) {
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax,
// TODO: Need to think about constants in test files. Maybe write type_string_test.go
// in a separate pass? For later.
Tests: false,
BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))},
}
pkgs, err := packages.Load(cfg, patterns...)
if err != nil {
log.Fatal(err)
}
if len(pkgs) != 1 {
log.Fatalf("error: %d packages matching %v", len(pkgs), strings.Join(patterns, " "))
}
g.addPackage(pkgs[0])
}

// addPackage adds a type checked Package and its syntax files to the generator.
func (g *Generator) addPackage(pkg *packages.Package) {
log.Printf("found package %s\n", pkg)
g.pkg = &Package{
name: pkg.Name,
pkgPath: pkg.PkgPath,
typesInfo: pkg.TypesInfo,
files: make([]*File, len(pkg.Syntax)),
}

for i, file := range pkg.Syntax {
g.pkg.files[i] = &File{
file: file,
pkg: g.pkg,
}
}
Pkg *packages.Package // Package we are scanning.
buf bytes.Buffer // Accumulated output.
}

func (g *Generator) Generate(typeName string) {
log.Printf("generating template for type %s", typeName)

parsedType, err := parser.Parse(g.Pkg, typeName)
if err != nil {
log.Fatalf("failed to inspect package: %v", err)
}

tmpl, err := template.New("template").Parse(g.Template)
if err != nil {
log.Fatalf("failed to parse template: %v", err)
}
err = tmpl.Execute(&g.buf, nil)
err = tmpl.Execute(&g.buf, parsedType)
if err != nil {
log.Fatalf("failed to execute template: %v", err)
}

log.Printf("generated buffer (%d bytes)", g.buf.Len())
}

// Format returns the gofmt-ed contents of the Generator's buffer.
func (g *Generator) Format() []byte {
log.Print("gofmt-ing buffer")

src, err := format.Source(g.buf.Bytes())
if err != nil {
// Should never happen, but can arise when developing this code.
// The user can compile the output to see the error.
log.Printf("warning: internal error: invalid Go generated: %s", err)
log.Printf("warning: compile the package to analyze the error")
return g.buf.Bytes()
Expand Down
41 changes: 41 additions & 0 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package parser

import (
"fmt"
"go/types"

"golang.org/x/tools/go/packages"
)

func Parse(pkg *packages.Package, typeName string) (ParsedType, error) {
p := ParsedType{
Type: Type(typeName),
}

for ident, obj := range pkg.TypesInfo.Defs {
if ident.Name == typeName {
structType, isStruct := obj.Type().Underlying().(*types.Struct)
if !isStruct {
return ParsedType{}, fmt.Errorf("type %s is not a struct", typeName)
}

p.Attributes = structAttributes(structType)
}
}

return p, nil
}

func structAttributes(structType *types.Struct) []Attributes {
var attributes []Attributes
for i := 0; i < structType.NumFields(); i++ {

attribute := structType.Field(i)
attributes = append(attributes, Attributes{
Name: attribute.Name(),
Type: Type(attribute.Origin().Type().String()),
})
}

return attributes
}
18 changes: 18 additions & 0 deletions internal/parser/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package parser

type (
ParsedType struct {
Type Type

Attributes []Attributes
}

Attributes struct {
Name string
Type Type

Keys []map[string]string
}

Type string
)
22 changes: 21 additions & 1 deletion internal/utils/helper.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
package utils

import (
"fmt"
"log"
"os"
"strings"

"golang.org/x/tools/go/packages"
)

// IsDirectory reports whether the named file is a directory.
func LoadPackage(patterns []string, tags []string) *packages.Package {
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax,
Tests: false,
BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))},
}
pkgs, err := packages.Load(cfg, patterns...)
if err != nil {
log.Fatal(err)
}
if len(pkgs) != 1 {
log.Fatalf("error: %d packages matching %v", len(pkgs), strings.Join(patterns, " "))
}

return pkgs[0]
}

func IsDirectory(name string) bool {
info, err := os.Stat(name)
if err != nil {
Expand Down

0 comments on commit 92f7c78

Please sign in to comment.