Skip to content

Commit 599fe73

Browse files
authored
Feature: Add NewFileRefreshableWithReaderFunc for custom file processing (#414)
Add NewFileRefreshableWithReaderFunc for custom file processing
1 parent 2ceb686 commit 599fe73

2 files changed

Lines changed: 78 additions & 3 deletions

File tree

refreshable/refreshable_file.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,27 @@ func NewFileRefreshable(ctx context.Context, filePath string) Validated[[]byte]
2323
// NewFileRefreshableWithTicker returns a Validated refreshable whose current value is the bytes of the file at the provided path.
2424
// This function reads the file once then starts a goroutine which re-reads the file on each tick until the provided context is cancelled.
2525
// If reading the file fails, the Current() value will be unchanged. The error is present in v.Validation().
26+
// It is equivalent to calling NewFileRefreshableWithReaderFunc with os.ReadFile.
2627
func NewFileRefreshableWithTicker(ctx context.Context, filePath string, updateTicker <-chan time.Time) Validated[[]byte] {
28+
return NewFileRefreshableWithReaderFunc(ctx, filePath, updateTicker, os.ReadFile)
29+
}
30+
31+
// NewFileRefreshableWithReaderFunc returns a [Validated] refreshable whose current value is the bytes read using the provided readerFunc.
32+
// This function is similar to [NewFileRefreshableWithTicker] but allows callers to provide a custom file reading function
33+
// instead of using os.ReadFile directly. This is useful for scenarios where custom file processing is needed
34+
// (e.g., decompression, decryption, or other transformations).
35+
//
36+
// The readerFunc is called once initially and then on each tick until the context is cancelled.
37+
// If reading fails, the Current() value will be unchanged. The error is present in v.Validation().
38+
func NewFileRefreshableWithReaderFunc(ctx context.Context, filePath string, updateTicker <-chan time.Time, readerFunc func(string) ([]byte, error)) Validated[[]byte] {
2739
v := newValidRefreshable[[]byte]()
28-
updateValidRefreshable[string, []byte](v, filePath, os.ReadFile)
40+
updateValidRefreshable(v, filePath, readerFunc)
2941
go func() {
3042
for {
3143
select {
3244
case <-updateTicker:
33-
// Read file and update refreshable. If ReadFile fails, the error is present in v.Validation().
34-
updateValidRefreshable[string, []byte](v, filePath, os.ReadFile)
45+
// Read file and update refreshable. If readerFunc fails, the error is present in v.Validation().
46+
updateValidRefreshable(v, filePath, readerFunc)
3547
case <-ctx.Done():
3648
return
3749
}

refreshable/refreshable_file_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,69 @@ func TestNewFileRefreshable(t *testing.T) {
7272
}, time.Second, 10*time.Millisecond)
7373
}
7474

75+
func TestNewFileRefreshableWithReaderFunc(t *testing.T) {
76+
ctx := t.Context()
77+
78+
dir := t.TempDir()
79+
filename := filepath.Join(dir, "file.txt")
80+
ticker := make(chan time.Time, 1)
81+
82+
// Custom reader that uppercases the content
83+
uppercaseReader := func(path string) ([]byte, error) {
84+
content, err := os.ReadFile(path)
85+
if err != nil {
86+
return nil, err
87+
}
88+
// Convert to uppercase
89+
for i := range content {
90+
if content[i] >= 'a' && content[i] <= 'z' {
91+
content[i] = content[i] - 'a' + 'A'
92+
}
93+
}
94+
return content, nil
95+
}
96+
97+
refreshableFile := NewFileRefreshableWithReaderFunc(ctx, filename, ticker, uppercaseReader)
98+
99+
// Assert we start with IsNotExist error.
100+
curr, err := refreshableFile.Validation()
101+
require.Error(t, err)
102+
require.True(t, os.IsNotExist(err))
103+
require.Empty(t, curr)
104+
require.Empty(t, refreshableFile.Current())
105+
106+
// Create file with lowercase content.
107+
require.NoError(t, os.WriteFile(filename, []byte("test"), 0644))
108+
ticker <- time.Now()
109+
require.EventuallyWithT(t, func(t *assert.CollectT) {
110+
curr, err := refreshableFile.Validation()
111+
require.NoError(t, err)
112+
require.Equal(t, "TEST", string(curr)) // Should be uppercased
113+
require.Equal(t, "TEST", string(refreshableFile.Current()))
114+
}, time.Second, 10*time.Millisecond)
115+
116+
// Update file.
117+
require.NoError(t, os.WriteFile(filename, []byte("hello world"), 0644))
118+
ticker <- time.Now()
119+
require.EventuallyWithT(t, func(t *assert.CollectT) {
120+
curr, err := refreshableFile.Validation()
121+
require.NoError(t, err)
122+
require.Equal(t, "HELLO WORLD", string(curr)) // Should be uppercased
123+
require.Equal(t, "HELLO WORLD", string(refreshableFile.Current()))
124+
}, time.Second, 10*time.Millisecond)
125+
126+
// Delete file - Current() should retain last valid value.
127+
require.NoError(t, os.Remove(filename))
128+
ticker <- time.Now()
129+
require.EventuallyWithT(t, func(t *assert.CollectT) {
130+
curr, err := refreshableFile.Validation()
131+
require.Error(t, err)
132+
require.True(t, os.IsNotExist(err))
133+
require.Empty(t, curr)
134+
require.Equal(t, "HELLO WORLD", string(refreshableFile.Current()))
135+
}, time.Second, 10*time.Millisecond)
136+
}
137+
75138
func TestNewMultiFileRefreshable(t *testing.T) {
76139
ctx := t.Context()
77140
dir := t.TempDir()

0 commit comments

Comments
 (0)