Kurt Peek
Kurt Peek

Reputation: 57741

How to parse an embedded template in Go?

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

Answers (1)

Kurt Peek
Kurt Peek

Reputation: 57741

To convert mkopriva's comment to an answer, I needed to provide a second, variadic argument to ParseFS representing a glob pattern that would match the template file:

tmpl, err := template.New("foo.go.tmpl").ParseFS(templateFS, "*.go.tmpl")

Upvotes: 3

Related Questions