diff --git a/assets.go b/assets.go new file mode 100644 index 0000000000000000000000000000000000000000..b3ed90c1011a144565e9fc8f293473352ba6cba3 --- /dev/null +++ b/assets.go @@ -0,0 +1,168 @@ +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 +} diff --git a/pkg/assets/test/assets/invalid.txt b/assets/tests/invalid.txt similarity index 100% rename from pkg/assets/test/assets/invalid.txt rename to assets/tests/invalid.txt diff --git a/pkg/assets/test/assets/item.json b/assets/tests/item.json similarity index 100% rename from pkg/assets/test/assets/item.json rename to assets/tests/item.json diff --git a/pkg/assets/test/assets/items.yaml b/assets/tests/items.yaml similarity index 100% rename from pkg/assets/test/assets/items.yaml rename to assets/tests/items.yaml diff --git a/assets_test.go b/assets_test.go new file mode 100644 index 0000000000000000000000000000000000000000..25f33bb5e7d5d20fd01f25ed248e661fb6c19bf0 --- /dev/null +++ b/assets_test.go @@ -0,0 +1,86 @@ +package perxis + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type State int + +const ( + State1 State = iota + State2 +) + +type testEntry struct { + ID string + Enum State + Data map[string]interface{} + Struct *nested +} + +type nested struct { + Option *bool +} + +func TestFromFS(t *testing.T) { + tr := true + i1 := &testEntry{ + ID: "item1", + Enum: State2, + Data: map[string]interface{}{ + "obj": map[string]interface{}{"str": "value"}, + "arr": []interface{}{"str1", "str2"}, + }, + Struct: &nested{ + Option: &tr, + }, + } + + i2 := *i1 + i2.ID = "item2" + + i3 := *i1 + i3.ID = "item3" + + assets := NewAssets[*testEntry]() + r, err := assets.FromFS(os.DirFS("assets/tests")) + require.NoError(t, err) + require.Len(t, r, 3) + assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r) +} + +func TestFrom(t *testing.T) { + tr := true + i1 := &testEntry{ + ID: "item1", + Enum: State2, + Data: map[string]interface{}{ + "obj": map[string]interface{}{"str": "value"}, + "arr": []interface{}{"str1", "str2"}, + }, + Struct: &nested{ + Option: &tr, + }, + } + + i2 := *i1 + i2.ID = "item2" + + i3 := *i1 + i3.ID = "item3" + + assets := NewAssets[*testEntry]() + r, err := assets.From(os.DirFS("assets"), "tests") + require.NoError(t, err) + require.Len(t, r, 3) + assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r) + + r, err = assets.From(os.DirFS("assets"), "tests/items.yaml") + require.NoError(t, err) + require.Len(t, r, 2) + assert.Equal(t, []*testEntry{i1, &i2}, r) +} diff --git a/pkg/assets/config.go b/pkg/assets/config.go deleted file mode 100644 index 3c2865571f619fd7f48a731aa984d2b8a24deabb..0000000000000000000000000000000000000000 --- a/pkg/assets/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package assets - -import "io/fs" - -// Для использования без объявления конфигурации: -// package items -// var FromFS = assets.FromFS[*Item] -func FromFS[T any](fsys fs.FS) ([]T, error) { - return New[T]().FromFS(fsys) -} - -type Config[T any] struct { - Constructor func() T -} - -// Для использования с настройкой: -// package schema -// var FromFS = assets.New[*Schema]().WithConstructor(customFunc).FromFS -func New[T any]() *Config[T] { - return &Config[T]{ - Constructor: func() (t T) { return t }, - } -} - -func (c *Config[T]) WithConstructor(t func() T) *Config[T] { - c.Constructor = t - return c -} diff --git a/pkg/assets/files.go b/pkg/assets/files.go deleted file mode 100644 index dd200628d3421d27404210499a081c08ff47359b..0000000000000000000000000000000000000000 --- a/pkg/assets/files.go +++ /dev/null @@ -1,50 +0,0 @@ -package assets - -import ( - "io/fs" - "path/filepath" - - "git.perx.ru/perxis/perxis-go/pkg/errors" -) - -func (c *Config[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 := c.FromJSON(file) - if err != nil { - return nil, errors.Wrapf(err, "file '%s'", stat.Name()) - } - return []T{entry}, nil - - case ".yaml", ".yml": - entries, err := c.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 (c *Config[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 := c.FromFile(file); err == nil { - result = append(result, entries...) - } - return nil - }); err != nil { - return nil, err - } - - return result, nil -} diff --git a/pkg/assets/test/files_test.go b/pkg/assets/test/files_test.go deleted file mode 100644 index 908cd3d282a98c2b9da39ec2b7a42fcf20673b24..0000000000000000000000000000000000000000 --- a/pkg/assets/test/files_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package test - -import ( - "os" - "testing" - - "git.perx.ru/perxis/perxis-go/pkg/assets" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type State int - -const ( - State1 State = iota - State2 -) - -type testEntry struct { - ID string - Enum State - Data map[string]interface{} - Struct *nested -} - -type nested struct { - Option *bool -} - -func TestFromFS(t *testing.T) { - tr := true - i1 := &testEntry{ - ID: "item1", - Enum: State2, - Data: map[string]interface{}{ - "obj": map[string]interface{}{"str": "value"}, - "arr": []interface{}{"str1", "str2"}, - }, - Struct: &nested{ - Option: &tr, - }, - } - - i2 := *i1 - i2.ID = "item2" - - i3 := *i1 - i3.ID = "item3" - - r, err := assets.FromFS[*testEntry](os.DirFS("assets")) - require.NoError(t, err) - require.Len(t, r, 3) - assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r) -} diff --git a/pkg/assets/umarshal.go b/pkg/assets/umarshal.go deleted file mode 100644 index 53937ac6f3dcc5c18ab319c49f8e850667f342ac..0000000000000000000000000000000000000000 --- a/pkg/assets/umarshal.go +++ /dev/null @@ -1,47 +0,0 @@ -package assets - -import ( - "errors" - "io" - - jsoniter "github.com/json-iterator/go" - "gopkg.in/yaml.v3" -) - -func (c *Config[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 -} - -func (c *Config[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 -} diff --git a/pkg/clients/client.go b/pkg/clients/client.go index 86f9dc6c4951776b053b94966ecb7796a7688cd8..f38b5acc9b442be39139523bcd316947d5d54fe7 100644 --- a/pkg/clients/client.go +++ b/pkg/clients/client.go @@ -1,7 +1,5 @@ package clients -import "git.perx.ru/perxis/perxis-go/pkg/assets" - // Client - приложение имеющее доступ к API type Client struct { // Внутренний идентификатор клиента внутри системы @@ -28,8 +26,6 @@ type Client struct { RoleID string `json:"role_id" bson:"role_id"` } -var FromFS = assets.FromFS[*Client] - type OAuth struct { ClientID string `bson:"client_id,omitempty" json:"client_id,omitempty"` // Идентификатор клиента выданные IdP сервером, используется для идентификации клиента AuthID string `bson:"auth_id,omitempty" json:"auth_id,omitempty"` // Сервис, который используется для авторизации клиента diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index ae7878d56de53f423ea86c28fa2f263cef6cbd03..ee87e981e6d31bb39b52516870d3bc637182a5af 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -3,6 +3,7 @@ package extension import ( "context" + "git.perx.ru/perxis/perxis-go" "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/content" "git.perx.ru/perxis/perxis-go/pkg/errors" @@ -41,6 +42,9 @@ var ( ErrNotInstalled = errors.New("not installed") ErrUnknownExtension = errors.New("unknown extension") + + ManifestAssets = perxis.NewAssets[*ExtensionDescriptor]() + ManifestFromFile = ManifestAssets.MustOneFrom ) // Runnable описывает интерфейс сервиса с запуском и остановкой. Вызывается сервером расширений diff --git a/pkg/items/item.go b/pkg/items/item.go index ade303296edc848ffcbb3d9e334e24ada6c9ccb4..1a226a7627a0435a2b77480ebc2095e126dae37d 100644 --- a/pkg/items/item.go +++ b/pkg/items/item.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "git.perx.ru/perxis/perxis-go/pkg/assets" "git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/locales" @@ -135,8 +134,6 @@ type Item struct { Template bool `json:"template" bson:"template,omitempty"` } -var FromFS = assets.FromFS[*Item] - func NewItem(spaceID, envID, collID, id string, data map[string]interface{}, translations map[string]map[string]interface{}) *Item { return &Item{ ID: id, diff --git a/pkg/roles/role.go b/pkg/roles/role.go index 4e3cf063ec8c8536075829988172aba26dcbb96f..579a9664ca87382c7c7533ecf502df3d6cb5c990 100644 --- a/pkg/roles/role.go +++ b/pkg/roles/role.go @@ -4,7 +4,6 @@ import ( "context" "slices" - "git.perx.ru/perxis/perxis-go/pkg/assets" "git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/environments" "git.perx.ru/perxis/perxis-go/pkg/permission" @@ -36,8 +35,6 @@ type Role struct { AllowManagement bool `json:"allow_management" bson:"allow_management"` } -var FromFS = assets.FromFS[*Role] - func (r Role) Clone() *Role { return &Role{ ID: r.ID, diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 0788c9eecf388662dcdd4592a5a6cd2205454a2f..b61c696316030afd5f71c9611b05d0c1b7e6863b 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -4,7 +4,7 @@ import ( "context" "reflect" - "git.perx.ru/perxis/perxis-go/pkg/assets" + "git.perx.ru/perxis/perxis-go" "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/expr" "git.perx.ru/perxis/perxis-go/pkg/schema/field" @@ -33,7 +33,9 @@ var ( Validate = validate.Validate Evaluate = field.Evaluate - FromFS = assets.New[*Schema]().WithConstructor(func() *Schema { return New() }).FromFS + Assets = perxis.NewAssets[*Schema]().WithConstructor(func() *Schema { return New() }) + FromFS = Assets.FromFS + FromFile = Assets.FromFile ) func (s *Schema) Clone(reset bool) *Schema { diff --git a/pkg/setup/config.go b/pkg/setup/config.go index cb5368b012f89f555d156c43b433cce2f9cbccae..08e1c740323d4e709e0a2d905ed2b2af2d4ebc5b 100644 --- a/pkg/setup/config.go +++ b/pkg/setup/config.go @@ -4,6 +4,8 @@ import ( "errors" "io/fs" + "git.perx.ru/perxis/perxis-go" + "git.perx.ru/perxis/perxis-go/pkg/clients" "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/items" @@ -110,11 +112,12 @@ func (cfg *Config) WithClientsOptions(filter func(c *clients.Client) bool, opts // LoadItems загружает элементы из указанной файловой системы func (cfg *Config) LoadClients(fsys fs.FS, opt ...ClientsOption) (*Config, error) { - clients, err := clients.FromFS(fsys) + assets := perxis.NewAssets[*clients.Client]() + cls, err := assets.FromFS(fsys) if err != nil { return nil, err } - return cfg.AddClients(clients, opt...), nil + return cfg.AddClients(cls, opt...), nil } func (cfg *Config) MustLoadClients(fsys fs.FS, opt ...ClientsOption) *Config { @@ -141,11 +144,12 @@ func (c *Config) AddClient(client *clients.Client, opt ...ClientsOption) *Config // LoadItems загружает элементы из указанной файловой системы func (cfg *Config) LoadRoles(fsys fs.FS, opt ...RolesOption) (*Config, error) { - roles, err := roles.FromFS(fsys) + assets := perxis.NewAssets[*roles.Role]() + rls, err := assets.FromFS(fsys) if err != nil { return nil, err } - return cfg.AddRoles(roles, opt...), nil + return cfg.AddRoles(rls, opt...), nil } func (cfg *Config) MustLoadRoles(fsys fs.FS, opt ...RolesOption) *Config { @@ -249,7 +253,8 @@ func (cfg *Config) GetCollectionConfig(id string) *CollectionConfig { // LoadItems загружает элементы из указанной файловой системы func (cfg *Config) LoadItems(fsys fs.FS, opt ...ItemsOption) (*Config, error) { - itms, err := items.FromFS(fsys) + assets := perxis.NewAssets[*items.Item]() + itms, err := assets.FromFS(fsys) if err != nil { return nil, err }