package perxis import ( "io" "io/fs" "path/filepath" "git.perx.ru/perxis/perxis-go/pkg/errors" jsoniter "github.com/json-iterator/go" "gopkg.in/yaml.v3" ) // Assets предоставляет методы для загрузки данных из файловой системы type Assets[T any] struct { Constructor func() T } // NewAssets возвращает новый экземпляр загрузчика func NewAssets[T any]() *Assets[T] { return &Assets[T]{ Constructor: func() (t T) { return t }, } } type FromFSFunc[T any] func(fsys fs.FS) ([]T, error) type FromFileFunc[T any] func(file fs.File) ([]T, error) func (a *Assets[T]) Funcs() (FromFSFunc[T], FromFileFunc[T]) { return a.FromFS, a.FromFile } // WithConstructor устанавливает конструктор для создания новых экземпляров func (a *Assets[T]) WithConstructor(t func() T) *Assets[T] { a.Constructor = t return a } // MustFrom возвращает все записи в переданной файловой системе func (a *Assets[T]) MustFrom(fsys fs.FS, path string) []T { res, err := a.From(fsys, path) if err != nil { panic(err) } return res } // MustOneFrom возвращает одну запись из переданного файла func (a *Assets[T]) MustOneFrom(fsys fs.FS, path string) T { res, err := a.From(fsys, path) if err != nil { panic(err) } if len(res) == 0 { panic(errors.Errorf("no entries found")) } if len(res) > 1 { panic(errors.Errorf("multiple entries found")) } return res[0] } // From возвращает записи из переданного файла func (a *Assets[T]) From(fsys fs.FS, path string) ([]T, error) { f, err := fsys.Open(path) if err != nil { return nil, err } defer f.Close() stat, err := f.Stat() if err != nil { return nil, err } if stat.IsDir() { sub, err := fs.Sub(fsys, path) if err != nil { return nil, err } return a.FromFS(sub) } return a.FromFile(f) } // FromFile возвращает записи в переданном файле func (a *Assets[T]) FromFile(file fs.File) ([]T, error) { stat, err := file.Stat() if err != nil { return nil, err } switch filepath.Ext(stat.Name()) { case ".json": entry, err := a.FromJSON(file) if err != nil { return nil, errors.Wrapf(err, "file '%s'", stat.Name()) } return []T{entry}, nil case ".yaml", ".yml": entries, err := a.FromYAML(file) return entries, errors.Wrapf(err, "file '%s'", stat.Name()) } return nil, errors.Errorf("file '%s' must be in JSON or YAML format", stat.Name()) } // FromFS возвращает все записи в переданной файловой системе func (a *Assets[T]) FromFS(fsys fs.FS) (result []T, err error) { if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, _ error) error { file, err := fsys.Open(path) if err != nil { return err } defer file.Close() if entries, err := a.FromFile(file); err == nil { result = append(result, entries...) } return nil }); err != nil { return nil, err } return result, nil } // FromJSON возвращает запись из JSON func (c *Assets[T]) FromJSON(r io.Reader) (T, error) { entry := c.Constructor() data, err := io.ReadAll(r) if err != nil { return entry, err } err = jsoniter.Unmarshal(data, &entry) return entry, err } // FromYAML возвращает записи из YAML func (c *Assets[T]) FromYAML(r io.Reader) (result []T, err error) { decoder := yaml.NewDecoder(r) for { var data interface{} err = decoder.Decode(&data) if errors.Is(err, io.EOF) { break } if err != nil { return nil, err } json, err := jsoniter.Marshal(data) if err != nil { return nil, err } entry := c.Constructor() if err = jsoniter.Unmarshal(json, &entry); err != nil { return nil, err } result = append(result, entry) } return result, nil }