Skip to content

Commit 00e8a2a

Browse files
authored
Add httpmock, merge unmarshal into fileutil. (#434)
## Community Contribution License All community contributions in this pull request are licensed to the project maintainers under the terms of the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request I represent that I have the right to license the contributions to the project maintainers under the Apache 2 license. ## Summary Add httpmock as an opensource package. It seems generally helpful, and we'll need it for aisdk. Decided to merge `unmarshal` into `fileutil` .... but let me know if you disagree. In the process, added better docs, tests, and added a Glob function to fileutil. ## How was it tested? Ran tests.
1 parent 2c3b404 commit 00e8a2a

13 files changed

Lines changed: 1360 additions & 126 deletions

File tree

pkg/fileutil/doc.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Package fileutil provides utilities for working with files and paths in a
2+
// safe and convenient way.
3+
//
4+
// The package offers two main sets of functionality:
5+
//
6+
// 1. File operations through both direct functions and a Path type:
7+
// - Checking file/directory existence and type
8+
// - Creating directories
9+
// - Writing files atomically
10+
// - Getting file information
11+
// - Globbing files with pattern matching
12+
//
13+
// 2. File unmarshaling utilities:
14+
// - Support for JSON, JSONC (JSON with comments), YAML, and TOML formats
15+
// - Batch processing of multiple files
16+
// - Type-safe unmarshaling into Go structs
17+
//
18+
// # Path Type
19+
//
20+
// The Path type provides a type-safe way to work with filesystem paths:
21+
//
22+
// path := fileutil.Path("base/dir")
23+
// subpath := path.Subpath("nested", "path")
24+
// if subpath.IsDir() {
25+
// // Handle directory...
26+
// }
27+
//
28+
// # File Operations
29+
//
30+
// Basic file operations are available both as methods on Path and as standalone
31+
// functions:
32+
//
33+
// // Using Path type
34+
// path := fileutil.Path("config")
35+
// if path.Exists() {
36+
// info := path.FileInfo()
37+
// // ...
38+
// }
39+
//
40+
// // Using standalone functions
41+
// if fileutil.Exists("config") {
42+
// info := fileutil.FileInfo("config")
43+
// // ...
44+
// }
45+
//
46+
// # File Unmarshaling
47+
//
48+
// The package provides utilities for unmarshaling structured data from files:
49+
//
50+
// var config MyConfig
51+
// err := fileutil.UnmarshalFile("config.yaml", &config)
52+
//
53+
// // Or process multiple files
54+
// configs, err := fileutil.UnmarshalPaths[MyConfig](fs, []string{"configs"})
55+
//
56+
// All paths in this package are relative to the current working directory unless
57+
// specified otherwise.
58+
package fileutil

pkg/fileutil/fileutil.go

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,97 @@
1-
// TODO: publish as it's own shared package that other binaries can use.
2-
// Right now we have other copies in other binaries. For example, devbox
3-
// has its own copy.
4-
51
package fileutil
62

73
import (
84
"io/fs"
95
"os"
106
"path/filepath"
117

8+
"github.com/bmatcuk/doublestar/v4"
129
"github.com/google/renameio/v2"
1310
)
1411

15-
type Path string
16-
17-
func (p Path) String() string {
18-
return string(p)
19-
}
12+
type osFS struct{}
2013

21-
func (p Path) Subpath(elements ...string) Path {
22-
all := append([]string{p.String()}, elements...)
23-
return Path(filepath.Join(all...))
14+
func (osFS) Open(name string) (fs.File, error) {
15+
return os.Open(name)
2416
}
2517

18+
// IsDir returns true if the path exists and is a directory.
2619
func IsDir(path string) bool {
27-
info := FileInfo(path)
28-
if info == nil {
29-
return false
30-
}
31-
return info.IsDir()
32-
}
33-
34-
func (p Path) IsDir() bool {
35-
return IsDir(p.String())
20+
return isDir(osFS{}, path)
3621
}
3722

23+
// IsFile returns true if the path exists and is a regular file.
3824
func IsFile(path string) bool {
39-
info := FileInfo(path)
40-
if info == nil {
41-
return false
42-
}
43-
return info.Mode().IsRegular()
25+
return isFile(osFS{}, path)
4426
}
4527

46-
func (p Path) IsFile() bool {
47-
return IsFile(p.String())
28+
// Exists returns true if the path exists.
29+
func Exists(path string) bool {
30+
return exists(osFS{}, path)
4831
}
4932

50-
func Exists(path string) bool {
51-
return FileInfo(path) != nil
33+
// FileInfo returns the fs.FileInfo for the given path.
34+
func FileInfo(path string) fs.FileInfo {
35+
return fileInfo(osFS{}, path)
5236
}
5337

54-
func (p Path) Exists() bool {
55-
return Exists(p.String())
38+
// Glob returns all files that match the given pattern.
39+
func Glob(pattern string) ([]string, error) {
40+
return doublestar.Glob(osFS{}, pattern)
5641
}
5742

43+
// EnsureDir ensures that the directory at the given path exists,
44+
// creating it and any parent directories if necessary.
5845
func EnsureDir(path string) error {
5946
if IsDir(path) {
6047
return nil
6148
}
6249
return os.MkdirAll(path, 0o700 /* as suggested by xdg spec */)
6350
}
6451

65-
func (p Path) EnsureDir() error {
66-
return EnsureDir(p.String())
52+
// WriteFile writes data to the named file, creating it if necessary.
53+
// If the file already exists, it is replaced.
54+
// The file is written atomically by writing to a temporary file and renaming it.
55+
func WriteFile(path string, data []byte) error {
56+
// First ensure the directory exists:
57+
dir := filepath.Dir(path)
58+
err := EnsureDir(dir)
59+
if err != nil {
60+
return err
61+
}
62+
63+
return renameio.WriteFile(path, data, 0o600)
64+
}
65+
66+
// isDir returns true if the path exists and is a directory in the given filesystem.
67+
func isDir(fsys fs.FS, path string) bool {
68+
info := fileInfo(fsys, path)
69+
if info == nil {
70+
return false
71+
}
72+
return info.IsDir()
6773
}
6874

69-
func FileInfo(path string) fs.FileInfo {
70-
info, err := os.Stat(path)
71-
if err != nil {
72-
return nil
75+
// isFile returns true if the path exists and is a regular file in the given filesystem.
76+
func isFile(fsys fs.FS, path string) bool {
77+
info := fileInfo(fsys, path)
78+
if info == nil {
79+
return false
7380
}
74-
return info
81+
return info.Mode().IsRegular()
7582
}
7683

77-
func (p Path) FileInfo() fs.FileInfo {
78-
return FileInfo(p.String())
84+
// exists returns true if the path exists in the given filesystem.
85+
func exists(fsys fs.FS, path string) bool {
86+
return fileInfo(fsys, path) != nil
7987
}
8088

81-
func WriteFile(path string, data []byte) error {
82-
// First ensure the directory exists:
83-
dir := filepath.Dir(path)
84-
err := EnsureDir(dir)
89+
// fileInfo returns the fs.FileInfo for the given path in the given filesystem.
90+
// Returns nil if the path does not exist or cannot be accessed.
91+
func fileInfo(fsys fs.FS, path string) fs.FileInfo {
92+
info, err := fs.Stat(fsys, path)
8593
if err != nil {
86-
return err
94+
return nil
8795
}
88-
// Write using `renameio` to ensure an atomic write:
89-
return renameio.WriteFile(path, data, 0o600)
96+
return info
9097
}

pkg/fileutil/fileutil_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package fileutil
2+
3+
import (
4+
"io/fs"
5+
"testing"
6+
"testing/fstest"
7+
)
8+
9+
func TestIsDir(t *testing.T) {
10+
fsys := fstest.MapFS{
11+
"dir/": &fstest.MapFile{Mode: fs.ModeDir},
12+
"file": &fstest.MapFile{Data: []byte("content")},
13+
"empty/": &fstest.MapFile{Mode: fs.ModeDir},
14+
}
15+
16+
tests := []struct {
17+
name string
18+
path string
19+
expected bool
20+
}{
21+
{"existing directory", "dir", true},
22+
{"regular file", "file", false},
23+
{"empty directory", "empty", true},
24+
{"non-existent path", "nonexistent", false},
25+
}
26+
27+
for _, tt := range tests {
28+
t.Run(tt.name, func(t *testing.T) {
29+
if got := isDir(fsys, tt.path); got != tt.expected {
30+
t.Errorf("isDir(%q) = %v, want %v", tt.path, got, tt.expected)
31+
}
32+
})
33+
}
34+
}
35+
36+
func TestIsFile(t *testing.T) {
37+
fsys := fstest.MapFS{
38+
"dir/": &fstest.MapFile{Mode: fs.ModeDir},
39+
"file": &fstest.MapFile{Data: []byte("content")},
40+
"empty": &fstest.MapFile{Data: []byte{}},
41+
}
42+
43+
tests := []struct {
44+
name string
45+
path string
46+
expected bool
47+
}{
48+
{"directory", "dir", false},
49+
{"regular file", "file", true},
50+
{"empty file", "empty", true},
51+
{"non-existent path", "nonexistent", false},
52+
}
53+
54+
for _, tt := range tests {
55+
t.Run(tt.name, func(t *testing.T) {
56+
if got := isFile(fsys, tt.path); got != tt.expected {
57+
t.Errorf("isFile(%q) = %v, want %v", tt.path, got, tt.expected)
58+
}
59+
})
60+
}
61+
}
62+
63+
func TestExists(t *testing.T) {
64+
fsys := fstest.MapFS{
65+
"dir/": &fstest.MapFile{Mode: fs.ModeDir},
66+
"file": &fstest.MapFile{Data: []byte("content")},
67+
"empty": &fstest.MapFile{Data: []byte{}},
68+
}
69+
70+
tests := []struct {
71+
name string
72+
path string
73+
expected bool
74+
}{
75+
{"existing directory", "dir", true},
76+
{"existing file", "file", true},
77+
{"empty file", "empty", true},
78+
{"non-existent path", "nonexistent", false},
79+
}
80+
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
if got := exists(fsys, tt.path); got != tt.expected {
84+
t.Errorf("exists(%q) = %v, want %v", tt.path, got, tt.expected)
85+
}
86+
})
87+
}
88+
}
89+
90+
func TestFileInfo(t *testing.T) {
91+
fsys := fstest.MapFS{
92+
"dir/": &fstest.MapFile{Mode: fs.ModeDir},
93+
"file": &fstest.MapFile{Data: []byte("content")},
94+
"empty": &fstest.MapFile{Data: []byte{}},
95+
}
96+
97+
tests := []struct {
98+
name string
99+
path string
100+
expectNil bool
101+
expectIsDir bool
102+
expectRegular bool
103+
}{
104+
{"directory", "dir", false, true, false},
105+
{"regular file", "file", false, false, true},
106+
{"empty file", "empty", false, false, true},
107+
{"non-existent path", "nonexistent", true, false, false},
108+
}
109+
110+
for _, test := range tests {
111+
t.Run(test.name, func(t *testing.T) {
112+
info := fileInfo(fsys, test.path)
113+
if test.expectNil {
114+
if info != nil {
115+
t.Errorf("fileInfo(%q) = %v, want nil", test.path, info)
116+
}
117+
return
118+
}
119+
if info == nil {
120+
t.Fatalf("fileInfo(%q) = nil, want non-nil", test.path)
121+
}
122+
if got := info.IsDir(); got != test.expectIsDir {
123+
t.Errorf("fileInfo(%q).IsDir() = %v, want %v", test.path, got, test.expectIsDir)
124+
}
125+
if got := info.Mode().IsRegular(); got != test.expectRegular {
126+
t.Errorf("fileInfo(%q).Mode().IsRegular() = %v, want %v", test.path, got, test.expectRegular)
127+
}
128+
})
129+
}
130+
}

pkg/fileutil/path.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package fileutil
2+
3+
import (
4+
"io/fs"
5+
"path/filepath"
6+
)
7+
8+
// Path represents a filesystem path with helper methods for common operations.
9+
// It provides a type-safe way to handle paths and perform path-related operations.
10+
type Path string
11+
12+
// String returns the path as a string.
13+
func (p Path) String() string {
14+
return string(p)
15+
}
16+
17+
// Subpath creates a new Path by joining the current path with additional path elements.
18+
// It uses filepath.Join internally to handle path separators correctly for the current OS.
19+
func (p Path) Subpath(elements ...string) Path {
20+
all := append([]string{p.String()}, elements...)
21+
return Path(filepath.Join(all...))
22+
}
23+
24+
// IsDir returns true if the path exists and is a directory.
25+
// The path is relative to the current working directory.
26+
func (p Path) IsDir() bool {
27+
return IsDir(p.String())
28+
}
29+
30+
// IsFile returns true if the path exists and is a regular file.
31+
// The path is relative to the current working directory.
32+
func (p Path) IsFile() bool {
33+
return IsFile(p.String())
34+
}
35+
36+
// EnsureDir ensures that the directory at this path exists,
37+
// creating it and any parent directories if necessary.
38+
func (p Path) EnsureDir() error {
39+
return EnsureDir(p.String())
40+
}
41+
42+
// FileInfo returns the fs.FileInfo for this path.
43+
// Returns nil if the path does not exist or cannot be accessed.
44+
// The path is relative to the current working directory.
45+
func (p Path) FileInfo() fs.FileInfo {
46+
return FileInfo(p.String())
47+
}
48+
49+
// Exists returns true if the path exists.
50+
// The path is relative to the current working directory.
51+
func (p Path) Exists() bool {
52+
return Exists(p.String())
53+
}

0 commit comments

Comments
 (0)