Skip to content

Commit

Permalink
Add FileSystem impl that expands Go templates
Browse files Browse the repository at this point in the history
Added a new FileSystem that evaluates/expands Go templates from *.ftmpl files in the base filesystem.
For example if the base filesystem contains file go.mod.ftmpl then the new resultant FS will contain go.mod file instead,
and the file would be result of template execution with templateData as the template data.

Signed-off-by: Matej Vašek <[email protected]>
  • Loading branch information
matejvasek committed Oct 21, 2024
1 parent 6c17586 commit 97fb956
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 0 deletions.
137 changes: 137 additions & 0 deletions pkg/filesystem/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package filesystem

import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"text/template"

billy "github.com/go-git/go-billy/v5"
)
Expand Down Expand Up @@ -253,6 +256,140 @@ func (m maskingFS) Readlink(link string) (string, error) {
return m.fs.Readlink(link)
}

// NewTemplatingFS creates a new filesystem that evaluates/expands Go templates from *.ftmpl files in the baseFS.
// For example if the baseFS contains file go.mod.ftmpl then the new resultant FS will contain go.mod file instead,
// and the file would be result of template execution with templateData as the template data.
func NewTemplatingFS(baseFS Filesystem, templateData any) templatingFS {
return templatingFS{
fs: baseFS,
templateData: templateData,
}
}

type templatingFS struct {
fs Filesystem
templateData any
}

type templatingFI struct {
fs.FileInfo
size int64
}

func (t templatingFI) Size() int64 {
return t.size
}

func (t templatingFI) Name() string {
return strings.TrimSuffix(t.FileInfo.Name(), ".ftmpl")
}

type templatingFile struct {
fi templatingFI
buff bytes.Buffer
}

func (t *templatingFile) Stat() (fs.FileInfo, error) {
return t.fi, nil
}

func (t *templatingFile) Read(i []byte) (int, error) {
return t.buff.Read(i)
}

func (t *templatingFile) Close() error {
return nil
}

func (t templatingFS) Open(name string) (fs.File, error) {
n := name + ".ftmpl"

if _, err := t.fs.Stat(n); errors.Is(err, fs.ErrNotExist) {
return t.fs.Open(name)
}

tmpl, err := template.ParseFS(t.fs, n)
if err != nil {
return nil, err
}

var f templatingFile
err = tmpl.Execute(&f.buff, t.templateData)
if err != nil {
return nil, err
}

fi, err := t.fs.Stat(n)
if err != nil {
return nil, err
}
f.fi = templatingFI{FileInfo: fi, size: int64(f.buff.Len())}

return &f, nil
}

func (t templatingFS) ReadDir(name string) ([]fs.DirEntry, error) {
des, err := t.fs.ReadDir(name)
if err != nil {
return nil, err
}

result := make([]fs.DirEntry, len(des))
for i, de := range des {
var fi fs.FileInfo
if !strings.HasSuffix(de.Name(), ".ftmpl") {
result[i] = de
continue
}

fi, err = t.Stat(path.Join(name, strings.TrimSuffix(de.Name(), ".ftmpl")))
if err != nil {
return nil, err
}
result[i] = dirEntry{fi}

}
return result, nil
}

func (t templatingFS) Stat(name string) (fs.FileInfo, error) {
n := name + ".ftmpl"

fi, err := t.fs.Stat(n)
if errors.Is(err, fs.ErrNotExist) {
return t.fs.Stat(name)
}

var tmpl *template.Template
tmpl, err = template.ParseFS(t.fs, n)
if err != nil {
return nil, err
}

var w wc
err = tmpl.Execute(&w, t.templateData)
if err != nil {
return nil, err
}
return templatingFI{
FileInfo: fi,
size: w.written,
}, nil
}

type wc struct {
written int64
}

func (w *wc) Write(p []byte) (n int, err error) {
w.written += int64(len(p))
return len(p), nil
}

func (t templatingFS) Readlink(link string) (string, error) {
return t.fs.Readlink(link)
}

// CopyFromFS copies files from the `src` dir on the accessor Filesystem to local filesystem into `dest` dir.
// The src path uses slashes as their separator.
// The dest path uses OS specific separator.
Expand Down
68 changes: 68 additions & 0 deletions pkg/filesystem/filesystem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,74 @@ func TestCopy(t *testing.T) {
}
}

func TestTemplatingFS(t *testing.T) {
tfs := filesystem.NewTemplatingFS(
mockFS{files: []FileInfo{
{
Path: "src",
Typ: fs.ModeDir,
},
{
Path: "src/hello.txt.ftmpl",
Content: []byte("Hi {{.Name}}!"),
},
}},
struct{ Name string }{Name: "John"},
)

expectedContent := "Hi John!"

fi, err := tfs.Stat("src/hello.txt")
if err != nil {
t.Fatal(err)
}
if fi.Size() != int64(len(expectedContent)) {
t.Errorf("size missmatch")
}

des, err := tfs.ReadDir("src")
if err != nil {
t.Fatal(err)
}
if len(des) != 1 {
t.Fatal("expected exactly one item in directory")
}
if des[0].Name() != "hello.txt" {
t.Fatalf("unexpected file: %q", des[0].Name())
}
fi, err = des[0].Info()
if err != nil {
t.Fatal(err)
}
if fi.Size() != int64(len(expectedContent)) {
t.Error("size missmatch")
}

expectedFiles := []FileInfo{
{
Path: ".",
Typ: fs.ModeDir,
},
{
Path: "src",
Typ: fs.ModeDir,
},
{
Path: "src/hello.txt",
Content: []byte(expectedContent),
},
}

actualFiles, err := loadFS(tfs)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(expectedFiles, actualFiles); diff != "" {
t.Error("filesystem content missmatch (-want, +got):", diff)
}
}

// mock for testing symlink functionality
type mockFS struct {
files []FileInfo
Expand Down

0 comments on commit 97fb956

Please sign in to comment.