Skip to content

Commit 19f927b

Browse files
committed
Embed improvements
- Added a fsutil.Embed.Sub method - The lang package loads from root FS directory instead of forcing resources/lang, unless the FS implements fsutil.WorkingDirDS
1 parent b4b2652 commit 19f927b

File tree

7 files changed

+166
-18
lines changed

7 files changed

+166
-18
lines changed

lang/lang.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ func New() *Languages {
3333
return l
3434
}
3535

36-
// LoadAllAvailableLanguages loads every language directory
37-
// in the "resources/lang" directory if it exists.
36+
// LoadAllAvailableLanguages loads every available language directory.
37+
// If the given FS implements `fsutil.WorkingDirFS`, the directory
38+
// used will be "<working directory>/resources/lang".
3839
func (l *Languages) LoadAllAvailableLanguages(fs fsutil.FS) error {
39-
prefix := ""
40+
langDirectory := "."
4041
if wd, ok := fs.(fsutil.WorkingDirFS); ok {
4142
workingDir, err := wd.Getwd()
4243
if err != nil {
4344
return errors.New(err)
4445
}
45-
prefix = workingDir + "/"
46+
langDirectory = workingDir + "/resources/lang"
4647
}
47-
langDirectory := prefix + "resources/lang"
4848
return l.LoadDirectory(fs, langDirectory)
4949
}
5050

@@ -62,7 +62,8 @@ func (l *Languages) LoadDirectory(fs fsutil.FS, directory string) error {
6262

6363
for _, f := range files {
6464
if f.IsDir() {
65-
if err := l.load(fs, f.Name(), directory+"/"+f.Name()); err != nil {
65+
path := lo.Ternary(directory == ".", f.Name(), directory+"/"+f.Name())
66+
if err := l.load(fs, f.Name(), path); err != nil {
6667
return err
6768
}
6869
}

lang/lang_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func (suite *LangTestSuite) TestLoad() {
7474
lines: map[string]string{
7575
"malformed-request": "Malformed request",
7676
"malformed-json": "Malformed JSON",
77+
"test-load": "load UK",
7778
},
7879
validation: validationLines{
7980
rules: map[string]string{},
@@ -93,6 +94,7 @@ func (suite *LangTestSuite) TestLoad() {
9394
"custom-line": "Custom line",
9495
"placeholder": "Line with :placeholders",
9596
"many-placeholders": "Line with :count :placeholders",
97+
"test-load": "load US",
9698
},
9799
validation: validationLines{
98100
rules: map[string]string{

resources/lang/en-UK/locale.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"malformed-request": "Malformed request",
3-
"malformed-json": "Malformed JSON"
3+
"malformed-json": "Malformed JSON",
4+
"test-load": "load UK"
45
}

resources/lang/en-US/locale.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"malformed-json": "Malformed JSON",
44
"custom-line": "Custom line",
55
"placeholder": "Line with :placeholders",
6-
"many-placeholders": "Line with :count :placeholders"
6+
"many-placeholders": "Line with :count :placeholders",
7+
"test-load": "load US"
78
}

server_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,12 @@ func TestServer(t *testing.T) {
120120
cfg.Set("database.options", "mode=memory")
121121

122122
logger := slog.New(slog.NewHandler(false, &bytes.Buffer{}))
123+
langEmbed, err := fsutil.NewEmbed(resources).Sub("resources/lang")
124+
assert.NoError(t, err)
123125
opts := Options{
124126
Config: cfg,
125127
Logger: logger,
126-
LangFS: fsutil.Embed{FS: resources},
128+
LangFS: langEmbed,
127129
}
128130

129131
server, err := New(opts)
@@ -137,6 +139,8 @@ func TestServer(t *testing.T) {
137139
assert.Equal(t, "test_with_config", server.Config().GetString("app.name"))
138140
assert.Equal(t, logger, server.Logger)
139141
assert.ElementsMatch(t, []string{"en-US", "en-UK"}, server.Lang.GetAvailableLanguages())
142+
assert.Equal(t, "load US", server.Lang.Get("en-US", "test-load"))
143+
assert.Equal(t, "load UK", server.Lang.Get("en-UK", "test-load"))
140144
assert.NotNil(t, server.DB())
141145

142146
assert.Nil(t, server.CloseDB())

util/fsutil/fsutil.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package fsutil
22

33
import (
4-
"embed"
54
"io"
65
"io/fs"
76
"net/http"
@@ -181,16 +180,46 @@ type RemoveFS interface {
181180
RemoveAll(path string) error
182181
}
183182

184-
// Embed is an extension of `embed.FS` implementing `fs.StatFS`.
183+
// Embed is an extension of aimed at improving `embed.FS` by
184+
// implementing `fs.StatFS` and a `Sub()` function.
185185
type Embed struct {
186-
embed.FS
186+
FS fs.ReadDirFS
187+
}
188+
189+
// NewEmbed returns a new Embed with the given FS.
190+
func NewEmbed(fs fs.ReadDirFS) Embed {
191+
return Embed{
192+
FS: fs,
193+
}
194+
}
195+
196+
// Open opens the named file.
197+
//
198+
// When Open returns an error, it should be of type *PathError
199+
// with the Op field set to "open", the Path field set to name,
200+
// and the Err field describing the problem.
201+
//
202+
// Open should reject attempts to open names that do not satisfy
203+
// ValidPath(name), returning a *PathError with Err set to
204+
// ErrInvalid or ErrNotExist.
205+
func (e Embed) Open(name string) (fs.File, error) {
206+
return e.FS.Open(name)
207+
}
208+
209+
// ReadDir reads the named directory
210+
// and returns a list of directory entries sorted by filename.
211+
func (e Embed) ReadDir(name string) ([]fs.DirEntry, error) {
212+
return e.FS.ReadDir(name)
187213
}
188214

189215
// Stat returns a FileInfo describing the file.
190216
func (e Embed) Stat(name string) (fileinfo fs.FileInfo, err error) {
217+
if statsFS, ok := e.FS.(fs.StatFS); ok {
218+
return statsFS.Stat(name)
219+
}
191220
f, err := e.FS.Open(name)
192221
if err != nil {
193-
return nil, err
222+
return nil, errors.New(err)
194223
}
195224
defer func() {
196225
e := f.Close()
@@ -202,3 +231,17 @@ func (e Embed) Stat(name string) (fileinfo fs.FileInfo, err error) {
202231
fileinfo, err = f.Stat()
203232
return
204233
}
234+
235+
// Sub returns an Embed FS corresponding to the subtree rooted at dir.
236+
// Returns and error if the underlying sub FS doesn't implement `fs.ReadDirFS`.
237+
func (e Embed) Sub(dir string) (Embed, error) {
238+
sub, err := fs.Sub(e.FS, dir)
239+
if err != nil {
240+
return Embed{}, errors.NewSkip(err, 3)
241+
}
242+
subFS, ok := sub.(fs.ReadDirFS)
243+
if !ok {
244+
return Embed{}, errors.NewSkip("fsutil.Embed: cannot Sub, underlying sub FS doesn't implement fsutil.FS", 3)
245+
}
246+
return Embed{FS: subFS}, nil
247+
}

util/fsutil/fsutil_test.go

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ import (
1414
"runtime"
1515
"strings"
1616
"testing"
17+
"time"
1718

1819
_ "embed"
1920

2021
"github.com/stretchr/testify/assert"
22+
"goyave.dev/goyave/v5/util/errors"
2123
"goyave.dev/goyave/v5/util/fsutil/osfs"
24+
25+
stderrors "errors"
2226
)
2327

2428
func deleteFile(path string) {
@@ -243,8 +247,52 @@ func TestParseMultipartFiles(t *testing.T) {
243247
//go:embed osfs
244248
var resources embed.FS
245249

250+
type testStatFS struct {
251+
embed.FS
252+
}
253+
254+
type mockFileInfo struct{}
255+
256+
func (fs *mockFileInfo) Name() string { return "" }
257+
func (fs *mockFileInfo) Size() int64 { return 0 }
258+
func (fs *mockFileInfo) Mode() fs.FileMode { return 0 }
259+
func (fs *mockFileInfo) ModTime() time.Time { return time.Now() }
260+
func (fs *mockFileInfo) Sys() any { return nil }
261+
func (fs *mockFileInfo) IsDir() bool { return false }
262+
263+
func (t testStatFS) Stat(_ string) (fileinfo fs.FileInfo, err error) {
264+
return &mockFileInfo{}, nil
265+
}
266+
267+
type mockFile struct {
268+
name string
269+
}
270+
271+
func (f *mockFile) Stat() (fs.FileInfo, error) { return nil, nil }
272+
func (f *mockFile) Read(_ []byte) (int, error) { return 0, nil }
273+
func (f *mockFile) Close() error { return nil }
274+
275+
type mockDirEntry struct{}
276+
277+
func (f *mockDirEntry) Name() string { return "" }
278+
func (f *mockDirEntry) IsDir() bool { return false }
279+
func (f *mockDirEntry) Type() fs.FileMode { return 0 }
280+
func (f *mockDirEntry) Info() (fs.FileInfo, error) { return &mockFileInfo{}, nil }
281+
282+
type mockFS struct{}
283+
284+
func (e mockFS) Open(name string) (fs.File, error) {
285+
return &mockFile{
286+
name: name,
287+
}, nil
288+
}
289+
290+
func (e mockFS) ReadDir(_ string) ([]fs.DirEntry, error) {
291+
return []fs.DirEntry{&mockDirEntry{}}, nil
292+
}
293+
246294
func TestEmbed(t *testing.T) {
247-
e := Embed{FS: resources}
295+
e := NewEmbed(resources)
248296

249297
stat, err := e.Stat("osfs/osfs.go")
250298
if !assert.NoError(t, err) {
@@ -256,9 +304,57 @@ func TestEmbed(t *testing.T) {
256304
stat, err = e.Stat("notadir/osfs.go")
257305
assert.Nil(t, stat)
258306
if assert.NotNil(t, err) {
259-
e, ok := err.(*fs.PathError)
260-
assert.True(t, ok)
261-
assert.Equal(t, "open", e.Op)
262-
assert.Equal(t, "notadir/osfs.go", e.Path)
307+
e, ok := err.(*errors.Error)
308+
if assert.True(t, ok) {
309+
var fsErr *fs.PathError
310+
if assert.True(t, stderrors.As(e, &fsErr)) {
311+
assert.Equal(t, "open", fsErr.Op)
312+
assert.Equal(t, "notadir/osfs.go", fsErr.Path)
313+
}
314+
}
263315
}
316+
317+
// Make it so the underlying FS implements
318+
e.FS = testStatFS{resources}
319+
stat, err = e.Stat("osfs/osfs.go")
320+
if !assert.NoError(t, err) {
321+
return
322+
}
323+
_, ok := stat.(*mockFileInfo)
324+
assert.True(t, ok)
325+
326+
t.Run("Open", func(t *testing.T) {
327+
e := NewEmbed(&mockFS{})
328+
329+
f, err := e.Open("")
330+
assert.NoError(t, err)
331+
_, ok := f.(*mockFile)
332+
assert.True(t, ok)
333+
})
334+
t.Run("ReadDir", func(t *testing.T) {
335+
e := NewEmbed(&mockFS{})
336+
337+
f, err := e.ReadDir("")
338+
assert.NoError(t, err)
339+
if assert.Len(t, f, 1) {
340+
_, ok := f[0].(*mockDirEntry)
341+
assert.True(t, ok)
342+
}
343+
})
344+
}
345+
346+
func TestEmbedSub(t *testing.T) {
347+
t.Run("err", func(t *testing.T) {
348+
e := NewEmbed(resources)
349+
sub, err := e.Sub("..")
350+
assert.Equal(t, Embed{}, sub)
351+
assert.Error(t, err)
352+
})
353+
354+
t.Run("Valid", func(t *testing.T) {
355+
e := NewEmbed(resources)
356+
sub, err := e.Sub("osfs.go") // It is valid to do this
357+
assert.NotNil(t, sub.FS)
358+
assert.NoError(t, err)
359+
})
264360
}

0 commit comments

Comments
 (0)