Reputation: 57741
I'm trying to write a Go program that generates code and to use the embed
package together with ParseFS
to parse the template. The code should ultimately satisfy the requirement that it can be run from any directory in the repository.
So far, I have the following working implementation using ParseFiles
. Using this directory structure,
.
├── codegen
│ └── main.go
├── foo
│ ├── foo.go
│ ├── foo.go.tmpl
│ └── foo_test.go
├── gen
│ └── foo.go
└── go.mod
The foo.go
file contains the code generation code,
package foo
import (
"bytes"
"fmt"
"html/template"
"path/filepath"
)
const templateFile = "../foo/foo.go.tmpl"
type GeneratedType struct {
Name string
StringFields []string
}
func GenerateCode() ([]byte, error) {
tmpl, err := template.New(filepath.Base(templateFile)).ParseFiles(templateFile)
if err != nil {
return nil, fmt.Errorf("parse template: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, GeneratedType{
Name: "Foo",
StringFields: []string{"Bar"},
}); err != nil {
return nil, fmt.Errorf("execute template: %v", err)
}
return buf.Bytes(), nil
}
where the template foo.go.tmpl
is
package foo
type {{.Name}} struct {
{{- range .StringFields}}
{{.}} string
{{- end}}
}
It also has a unit test, foo_test.go
:
package foo
import (
"go/format"
"testing"
)
func TestGenerateCode(t *testing.T) {
code, err := GenerateCode()
if err != nil {
t.Errorf("generate code: %v", err)
}
if _, err := format.Source(code); err != nil {
t.Errorf("format source code: %v", err)
}
}
The codegen/main.go
contains is run with Go's generate feature and contains the invocation of GenerateCode
that generates code in an output directory gen
:
//go:generate go run github.com/khpeek/codegen-example/codegen
package main
import (
"errors"
"log"
"os"
"github.com/khpeek/codegen-example/foo"
)
func main() {
code, err := foo.GenerateCode()
if err != nil {
log.Fatalf("generate code: %v", err)
}
if err := os.Mkdir("../gen", 0700); err != nil && !errors.Is(err, os.ErrExist) {
log.Fatalf("create directory for generated code: %v", err)
}
if err := os.WriteFile("../gen/foo.go", code, 0644); err != nil {
log.Fatalf("write file: %v", err)
}
}
This works in that I can call both go generate ./...
and go test ./...
to generate the code and run tests successfully. However, it is a bit fragile because the template file path ../foo/foo.go.tmpl
only "coincidentally" resolves correctly from both the codegen
and the foo
directory. If I were to change the directory level, I suspect this example would no longer work.
I would like to make this more robust by using the embed
package so that I'm always referencing files in the package directory (foo
in this case). To that end, I attempted to change foo.go
into the following:
package foo
import (
"bytes"
"embed"
"fmt"
"text/template"
)
//go:embed foo.go.tmpl
var templateFS embed.FS
type GeneratedType struct {
Name string
StringFields []string
}
func GenerateCode() ([]byte, error) {
tmpl, err := template.New("foo.go.tmpl").ParseFS(templateFS)
if err != nil {
return nil, fmt.Errorf("parse template: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, GeneratedType{
Name: "Foo",
StringFields: []string{"Bar"},
}); err != nil {
return nil, fmt.Errorf("execute template: %v", err)
}
return buf.Bytes(), nil
}
Now however, when I try to generate code or run unit tests I get a no files name in call to ParseFiles
error:
> go generate ./...
2023/01/31 09:06:21 generate code: parse template: template: no files named in call to ParseFiles
exit status 1
codegen/main.go:1: running "go": exit status 1
> go test ./...
? github.com/khpeek/codegen-example/codegen [no test files]
--- FAIL: TestGenerateCode (0.00s)
foo_test.go:11: generate code: parse template: template: no files named in call to ParseFiles
FAIL
FAIL github.com/khpeek/codegen-example/foo 0.137s
? github.com/khpeek/codegen-example/gen [no test files]
FAIL
Can anyone explain why ParseFS
isn't "finding" the template file?
Upvotes: 4
Views: 2175