Skip to content

Commit b7b4138

Browse files
committed
improve MemoryFileSystem to adjust all use cases and add a simple end-to-end test
1 parent bff2a42 commit b7b4138

File tree

2 files changed

+292
-16
lines changed

2 files changed

+292
-16
lines changed

Diff for: fs.go

+186-16
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"io/fs"
88
"net/http"
99
"os"
10+
"path"
1011
"path/filepath"
12+
"strings"
1113
"time"
1214
)
1315

@@ -124,17 +126,21 @@ type MemoryFileSystem struct {
124126
// It comes with no files, use `ParseTemplate` to add new virtual/memory template files.
125127
// Usage:
126128
//
127-
// vfs := NewVirtualFileSystem()
128-
// err := vfs.ParseTemplate("example.html", []byte("Hello, World!"), nil)
129-
// templates := New(vfs)
129+
// mfs := NewVirtualFileSystem()
130+
// err := mfs.ParseTemplate("example.html", []byte("Hello, World!"), nil)
131+
// templates := New(mfs)
130132
// templates.Load()
131133
func NewMemoryFileSystem() *MemoryFileSystem {
132134
return &MemoryFileSystem{
133135
files: make(map[string]*memoryTemplateFile),
134136
}
135137
}
136138

137-
var _ fs.FS = (*MemoryFileSystem)(nil)
139+
// Ensure MemoryFileSystem implements fs.FS, fs.ReadDirFS and fs.WalkDirFS interfaces.
140+
var (
141+
_ fs.FS = (*MemoryFileSystem)(nil)
142+
_ fs.ReadDirFS = (*MemoryFileSystem)(nil)
143+
)
138144

139145
// ParseTemplate adds a new memory temlate to the file system.
140146
func (vfs *MemoryFileSystem) ParseTemplate(name string, contents []byte, funcMap template.FuncMap) error {
@@ -147,14 +153,178 @@ func (vfs *MemoryFileSystem) ParseTemplate(name string, contents []byte, funcMap
147153
}
148154

149155
// Open implements the fs.FS interface.
150-
func (vfs *MemoryFileSystem) Open(name string) (fs.File, error) {
151-
if file, exists := vfs.files[name]; exists {
156+
func (mfs *MemoryFileSystem) Open(name string) (fs.File, error) {
157+
if name == "." || name == "/" {
158+
// Return a directory representing the root.
159+
return &memoryDir{
160+
fs: mfs,
161+
name: ".",
162+
}, nil
163+
}
164+
165+
if mfs.isDir(name) {
166+
// Return a directory.
167+
return &memoryDir{
168+
fs: mfs,
169+
name: name,
170+
}, nil
171+
}
172+
173+
if file, exists := mfs.files[name]; exists {
152174
file.reset() // Reset read position
153175
return file, nil
154176
}
177+
155178
return nil, fs.ErrNotExist
156179
}
157180

181+
// ReadDir implements the fs.ReadDirFS interface.
182+
func (mfs *MemoryFileSystem) ReadDir(name string) ([]fs.DirEntry, error) {
183+
var entries []fs.DirEntry
184+
prefix := strings.TrimLeftFunc(name, func(r rune) bool {
185+
return r == '.' || r == '/'
186+
})
187+
if prefix != "" && !strings.HasSuffix(prefix, "/") {
188+
prefix += "/"
189+
}
190+
191+
seen := make(map[string]bool)
192+
193+
for path := range mfs.files {
194+
if !strings.HasPrefix(path, prefix) {
195+
continue
196+
}
197+
198+
trimmedPath := strings.TrimPrefix(path, prefix)
199+
parts := strings.SplitN(trimmedPath, "/", 2)
200+
entryName := parts[0]
201+
202+
if seen[entryName] {
203+
continue
204+
}
205+
seen[entryName] = true
206+
207+
fullPath := prefix + entryName
208+
if mfs.isDir(fullPath) {
209+
info := &memoryDirInfo{name: entryName}
210+
entries = append(entries, fs.FileInfoToDirEntry(info))
211+
} else {
212+
file, _ := mfs.files[fullPath]
213+
info := &memoryFileInfo{
214+
name: entryName,
215+
size: int64(len(file.contents)),
216+
}
217+
entries = append(entries, fs.FileInfoToDirEntry(info))
218+
}
219+
}
220+
221+
return entries, nil
222+
}
223+
224+
// isDir checks if the given name is a directory in the memory file system.
225+
func (mfs *MemoryFileSystem) isDir(name string) bool {
226+
dirPrefix := name
227+
if dirPrefix != "" && !strings.HasSuffix(dirPrefix, "/") {
228+
dirPrefix += "/"
229+
}
230+
for path := range mfs.files {
231+
if strings.HasPrefix(path, dirPrefix) {
232+
return true
233+
}
234+
}
235+
return false
236+
}
237+
238+
type memoryDir struct {
239+
fs *MemoryFileSystem
240+
name string
241+
offset int
242+
entries []fs.DirEntry
243+
}
244+
245+
// Ensure memoryDir implements fs.ReadDirFile interface.
246+
var _ fs.ReadDirFile = (*memoryDir)(nil)
247+
248+
// Read implements the io.Reader interface.
249+
func (d *memoryDir) Read(p []byte) (int, error) {
250+
return 0, io.EOF // Directories cannot be read as files.
251+
}
252+
253+
// Close implements the io.Closer interface.
254+
func (d *memoryDir) Close() error {
255+
return nil
256+
}
257+
258+
// Stat implements the fs.File interface.
259+
func (d *memoryDir) Stat() (fs.FileInfo, error) {
260+
return &memoryDirInfo{
261+
name: d.name,
262+
}, nil
263+
}
264+
265+
// ReadDir implements the fs.ReadDirFile interface.
266+
func (d *memoryDir) ReadDir(n int) ([]fs.DirEntry, error) {
267+
if d.entries == nil {
268+
// Initialize the entries slice.
269+
entries, err := d.fs.ReadDir(d.name)
270+
if err != nil {
271+
return nil, err
272+
}
273+
d.entries = entries
274+
}
275+
276+
if d.offset >= len(d.entries) {
277+
return nil, io.EOF
278+
}
279+
280+
if n <= 0 || d.offset+n > len(d.entries) {
281+
n = len(d.entries) - d.offset
282+
}
283+
284+
entries := d.entries[d.offset : d.offset+n]
285+
d.offset += n
286+
287+
return entries, nil
288+
}
289+
290+
// memoryDirInfo provides directory information for a memory directory.
291+
type memoryDirInfo struct {
292+
name string
293+
}
294+
295+
// Ensure memoryDirInfo implements fs.FileInfo interface.
296+
var _ fs.FileInfo = (*memoryDirInfo)(nil)
297+
298+
// Name returns the base name of the directory.
299+
func (di *memoryDirInfo) Name() string {
300+
return di.name
301+
}
302+
303+
// Size returns the length in bytes (zero for directories).
304+
func (di *memoryDirInfo) Size() int64 {
305+
return 0
306+
}
307+
308+
// Mode returns file mode bits.
309+
func (di *memoryDirInfo) Mode() fs.FileMode {
310+
return fs.ModeDir | 0555 // Readable directory
311+
}
312+
313+
// ModTime returns modification time.
314+
func (di *memoryDirInfo) ModTime() time.Time {
315+
return time.Now()
316+
}
317+
318+
// IsDir reports if the file is a directory.
319+
func (di *memoryDirInfo) IsDir() bool {
320+
return true
321+
}
322+
323+
// Sys returns underlying data source (can return nil).
324+
func (di *memoryDirInfo) Sys() interface{} {
325+
return nil
326+
}
327+
158328
// memoryTemplateFile represents a memory file.
159329
type memoryTemplateFile struct {
160330
name string
@@ -166,30 +336,30 @@ type memoryTemplateFile struct {
166336
// Ensure memoryTemplateFile implements fs.File interface.
167337
var _ fs.File = (*memoryTemplateFile)(nil)
168338

169-
func (vf *memoryTemplateFile) reset() {
170-
vf.offset = 0
339+
func (mf *memoryTemplateFile) reset() {
340+
mf.offset = 0
171341
}
172342

173343
// Stat implements the fs.File interface, returning file info.
174-
func (vf *memoryTemplateFile) Stat() (fs.FileInfo, error) {
344+
func (mf *memoryTemplateFile) Stat() (fs.FileInfo, error) {
175345
return &memoryFileInfo{
176-
name: vf.name,
177-
size: int64(len(vf.contents)),
346+
name: path.Base(mf.name),
347+
size: int64(len(mf.contents)),
178348
}, nil
179349
}
180350

181351
// Read implements the io.Reader interface.
182-
func (vf *memoryTemplateFile) Read(p []byte) (int, error) {
183-
if vf.offset >= int64(len(vf.contents)) {
352+
func (mf *memoryTemplateFile) Read(p []byte) (int, error) {
353+
if mf.offset >= int64(len(mf.contents)) {
184354
return 0, io.EOF
185355
}
186-
n := copy(p, vf.contents[vf.offset:])
187-
vf.offset += int64(n)
356+
n := copy(p, mf.contents[mf.offset:])
357+
mf.offset += int64(n)
188358
return n, nil
189359
}
190360

191361
// Close implements the io.Closer interface.
192-
func (vf *memoryTemplateFile) Close() error {
362+
func (mf *memoryTemplateFile) Close() error {
193363
return nil
194364
}
195365

Diff for: fs_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package blocks_test
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
"github.com/kataras/blocks"
9+
)
10+
11+
func TestMemoryFileSystem(t *testing.T) {
12+
// Create a new MemoryFileSystem
13+
mfs := blocks.NewMemoryFileSystem()
14+
15+
// Define template contents
16+
mainTemplateContent := []byte(`
17+
<!DOCTYPE html>
18+
<html>
19+
<head>
20+
<title>{{ .Title }}</title>
21+
</head>
22+
<body>
23+
{{ template "content" . }}
24+
{{- partial "custom/user/partial" . }}
25+
</body>
26+
</html>
27+
`)
28+
29+
contentTemplateContent := []byte(`{{ define "content" }}Hello, {{ .Name }}!{{ end }}`)
30+
partialTemplateContent := []byte(`<h3>Partial</h3>`)
31+
32+
// Parse templates into the memory file system
33+
err := mfs.ParseTemplate("layouts/main.html", mainTemplateContent, nil)
34+
if err != nil {
35+
t.Fatalf("Failed to parse main.html: %v", err)
36+
}
37+
38+
err = mfs.ParseTemplate("index.html", contentTemplateContent, nil)
39+
if err != nil {
40+
t.Fatalf("Failed to parse index.html: %v", err)
41+
}
42+
43+
err = mfs.ParseTemplate("custom/user/partial.html", partialTemplateContent, nil)
44+
if err != nil {
45+
t.Fatalf("Failed to parse partial.html: %v", err)
46+
}
47+
48+
// Create a new Blocks instance using the MemoryFileSystem
49+
views := blocks.New(mfs)
50+
51+
// Set the main layout file
52+
views.DefaultLayout("main")
53+
54+
// Load the templates
55+
err = views.Load()
56+
if err != nil {
57+
t.Fatalf("Failed to load templates: %v", err)
58+
}
59+
60+
// Data for template execution
61+
data := map[string]any{
62+
"Title": "Test Page",
63+
"Name": "World",
64+
}
65+
66+
// Execute the template
67+
var buf bytes.Buffer
68+
err = views.ExecuteTemplate(&buf, "index", "", data)
69+
if err != nil {
70+
t.Fatalf("Failed to execute template: %v", err)
71+
}
72+
73+
// Expected output
74+
expectedOutput := `
75+
<!DOCTYPE html>
76+
<html>
77+
<head>
78+
<title>Test Page</title>
79+
</head>
80+
<body>
81+
Hello, World! <h3>Partial</h3>
82+
</body>
83+
</html>
84+
`
85+
86+
// Trim whitespace for comparison
87+
expected := trimContents(expectedOutput)
88+
result := trimContents(buf.String())
89+
90+
if expected != result {
91+
t.Errorf("Expected output does not match.\nExpected:\n%s\nGot:\n%s", expected, result)
92+
}
93+
}
94+
95+
func trimContents(s string) string {
96+
trimLineFunc := func(r rune) bool {
97+
return r == '\r' || r == '\n' || r == ' ' || r == '\t' || r == '\v' || r == '\f'
98+
}
99+
100+
lines := strings.Split(s, "\n")
101+
for i, line := range lines {
102+
lines[i] = strings.TrimFunc(line, trimLineFunc)
103+
}
104+
105+
return strings.Join(lines, " ")
106+
}

0 commit comments

Comments
 (0)