Skip to content
Snippets Groups Projects
Commit 2ffe1832 authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

Merge branch 'feature/PRXS-2731-Assets' into 'master'

Реализована загрузка данных схем и записей через файлы для использования расширениях

See merge request perxis/perxis-go!302
parents fdab81fd a978cfa2
No related branches found
No related tags found
No related merge requests found
Showing
with 799 additions and 309 deletions
assets.go 0 → 100644
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
}
{
"id": "item3",
"enum": 1,
"data": {
"obj": {
"str": "value"
},
"arr": [
"str1",
"str2"
]
},
"struct": {
"option": true
}
}
\ No newline at end of file
---
id: item1
enum: 1
data:
obj:
str: value
arr:
- str1
- str2
struct:
option: true
---
id: item2
enum: 1
data:
obj:
str: value
arr:
- str1
- str2
struct:
option: true
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)
}
package collections
import (
"io/fs"
"time"
"git.perx.ru/perxis/perxis-go/pkg/optional"
......@@ -236,3 +237,13 @@ func FromSchemaMetadata(schemas ...*schema.Schema) []*Collection {
return result
}
// TODO: использовать загрузку из файлов коллекций вместо файлов схем
func FromFS(filesystem fs.FS) ([]*Collection, error) {
schemas, err := schema.FromFS(filesystem)
if err != nil {
return nil, err
}
return FromSchemaMetadata(schemas...), nil
}
......@@ -2,8 +2,8 @@ package extension
import (
"context"
"io/fs"
"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"
......@@ -42,6 +42,9 @@ var (
ErrNotInstalled = errors.New("not installed")
ErrUnknownExtension = errors.New("unknown extension")
ManifestAssets = perxis.NewAssets[*ExtensionDescriptor]()
ManifestFromFile = ManifestAssets.MustOneFrom
)
// Runnable описывает интерфейс сервиса с запуском и остановкой. Вызывается сервером расширений
......@@ -96,22 +99,3 @@ func UpdateCollectionStrategy(s *setup.Setup, exist, collection *collections.Col
}
return setup.DefaultUpdateCollectionStrategyFn(s, exist, collection)
}
// CollectionsFromAssets инициализирует коллекции из файловой системы
// В качестве файловой системы можно использовать файлы ОС `filesystem := os.DirFS("assets/schemas")`
// Или файловую систему, встроенную в исполняемый файл при компиляции:
//
// //go:embed assets/schemas/*
// var filesystem embed.FS
func CollectionsFromAssets(filesystem fs.FS, extension string) []*collections.Collection {
schemas, err := schema.FromFS(filesystem)
if err != nil {
panic(err)
}
for _, schema := range schemas {
schema.WithMetadata(MetadataKey, extension)
}
return collections.FromSchemaMetadata(schemas...)
}
......@@ -5,6 +5,7 @@ import (
"fmt"
"git.perx.ru/perxis/perxis-go/pkg/clients"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/content"
"git.perx.ru/perxis/perxis-go/pkg/extension"
"git.perx.ru/perxis/perxis-go/pkg/roles"
......@@ -134,6 +135,10 @@ func (s *Extension) setupExtensionClient(set *setup.Setup, spaceID string) {
func (s *Extension) GetSetup(spaceID, envID string) (*setup.Setup, error) {
set := s.setupFunc(spaceID, envID)
set.WithCollectionOptions(
func(c *collections.Collection) bool { return c.Schema != nil },
setup.AddMetadata(extension.MetadataKey, s.GetName()),
)
if set.HasErrors() {
s.Logger.Error("Invalid setup config", zap.Errors("Errors", set.Errors()))
return nil, set.Error()
......
......@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"net/url"
"reflect"
......@@ -65,14 +66,21 @@ func (t FileType) Encode(ctx context.Context, fld *field.Field, v interface{}) (
return nil, fmt.Errorf("FileField encode error: incorrect type: \"%s\", expected \"file\"", reflect.ValueOf(v).Kind())
}
if f.File != nil { // upload from file system
if f.Content != nil {
// По возможности устанавливаем указатель в начало,
// для избегания ошибки при повторном чтении
if seeker, ok := f.Content.(io.Seeker); ok {
_, _ = seeker.Seek(0, io.SeekStart)
}
upload, err := t.fs.Upload(ctx, f)
if err != nil {
return nil, err
}
if err = t.uploader.Upload(f.File, upload); err != nil {
if err = t.uploader.Upload(f.Content, upload); err != nil {
return nil, err
}
f = &upload.File
}
......
......@@ -3,7 +3,7 @@ package files
import (
"bytes"
"fmt"
"io/fs"
"io"
"strings"
"text/template"
......@@ -22,7 +22,7 @@ type File struct {
MimeType string `mapstructure:"mimeType,omitempty" json:"mimeType" bson:"mimeType,omitempty" expr:"mime_type"` // Mime-type файла
URL string `mapstructure:"url,omitempty" json:"url" bson:"url,omitempty" expr:"url"` // Адрес для загрузки файла
Key string `mapstructure:"key,omitempty" json:"key" bson:"key,omitempty" expr:"key"` // Ключ для хранения файла в хранилище
File fs.File `mapstructure:"-" json:"-" bson:"-"` // Файл для загрузки(из файловой системы)
Content io.Reader `mapstructure:"-" json:"-" bson:"-"` // Альтернативный способ задать содержимое файла
}
func (f File) Clone() *File {
......
......@@ -2,10 +2,9 @@ package schema
import (
"context"
"io/fs"
"path/filepath"
"reflect"
"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"
......@@ -27,50 +26,16 @@ func NewFromField(f *field.Field) *Schema {
return &Schema{Field: *f}
}
// FromFile инициализирует и возвращает объект схемы из файла
// Поддерживаются форматы JSON и YAML
func FromFile(file fs.File) (*Schema, error) {
stat, err := file.Stat()
if err != nil {
return nil, err
}
switch filepath.Ext(stat.Name()) {
case ".json":
return FromJSON(file)
case ".yaml", ".yml":
return FromYAML(file)
}
return nil, errors.New("schema must be in JSON or YAML format")
}
// FromFS возвращает все валидные схемы в переданной файловой системе
func FromFS(fsys fs.FS) (result []*Schema, 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 schema, err := FromFile(file); err == nil {
result = append(result, schema)
}
return nil
}); err != nil {
return nil, err
}
return result, nil
}
var (
Encode = field.Encode
Decode = field.Decode
Modify = modify.Modify
Validate = validate.Validate
Evaluate = field.Evaluate
Assets = perxis.NewAssets[*Schema]().WithConstructor(func() *Schema { return New() })
FromFS = Assets.FromFS
FromFile = Assets.FromFile
)
func (s *Schema) Clone(reset bool) *Schema {
......@@ -295,3 +260,12 @@ func (s *Schema) Introspect(ctx context.Context, data map[string]interface{}) (m
return val, mutatedSchema, nil
}
// GetEnum возвращает список опций перечисления для поля
func (s *Schema) GetEnum(fieldPath string) []validate.EnumOpt {
f := s.Field.GetField(fieldPath)
if f == nil {
return nil
}
return validate.GetEnum(f)
}
package schema
import (
"io"
jsoniter "github.com/json-iterator/go"
"gopkg.in/yaml.v3"
)
func FromYAML(r io.Reader) (s *Schema, err error) {
yml, err := io.ReadAll(r)
if err != nil {
return nil, err
}
s = New()
err = s.UnmarshalYAML(yml)
return s, err
}
func (s *Schema) UnmarshalYAML(b []byte) error {
jsonData, err := yamlToJson(b)
if err != nil {
return err
}
return s.UnmarshalJSON(jsonData)
}
func (s *Schema) MarshalYAML() ([]byte, error) {
jsonData, err := s.MarshalJSON()
if err != nil {
return nil, err
}
return jsonToYaml(jsonData)
}
func jsonToYaml(b []byte) ([]byte, error) {
var data interface{}
if err := jsoniter.Unmarshal(b, &data); err != nil {
return nil, err
}
return yaml.Marshal(data)
}
func yamlToJson(b []byte) ([]byte, error) {
var data interface{}
if err := yaml.Unmarshal(b, &data); err != nil {
return nil, err
}
return jsoniter.Marshal(data)
}
......@@ -10,50 +10,9 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
"git.perx.ru/perxis/perxis-go/pkg/schema/modify"
"git.perx.ru/perxis/perxis-go/pkg/schema/validate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFromFile(t *testing.T) {
for _, tt := range []struct {
path string
wantSchema *schema.Schema
wantErr string
}{
{
path: "assets/not_schema.txt",
wantErr: "schema must be in JSON or YAML format",
},
{
path: "assets/invalid.json",
wantErr: "error unmarshal json into field",
},
{
path: "assets/web_pages.json",
wantSchema: getPagesSchema(),
},
{
path: "assets/web_pages.yml",
wantSchema: getPagesSchema(),
},
} {
t.Run(tt.path, func(t *testing.T) {
file, err := os.Open(tt.path)
require.NoError(t, err)
result, err := schema.FromFile(file)
if tt.wantErr != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantSchema, result)
})
}
}
func TestFromFiles(t *testing.T) {
t.Run("Non-existen path", func(t *testing.T) {
schemas, err := schema.FromFS(os.DirFS("non-existen"))
......@@ -66,7 +25,7 @@ func TestFromFiles(t *testing.T) {
schemas, err := schema.FromFS(os.DirFS("assets"))
require.NoError(t, err)
require.Len(t, schemas, 2, "В директории хранятся две корректные схемы")
require.Equal(t, schemas[0], schemas[1], "Они одинаковые, но в разных форматах")
require.ElementsMatch(t, []*schema.Schema{getPagesSchema(), getPagesSchema()}, schemas, "Cхемы должны соответствовать объекту из функции")
})
}
......
......@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
......@@ -293,82 +292,6 @@ func TestSchemaUI_UnmarshalJSON(t *testing.T) {
assert.Equal(t, sch, schm)
}
func TestSchemaUI_UnmarshalYAML(t *testing.T) {
vw := &field.View{
Widget: "Widget",
Options: map[string]interface{}{"title": "name", "key": "name"},
}
ui := &field.UI{
Widget: "Widget",
Placeholder: "Placeholder",
Options: map[string]interface{}{"title": "name", "key": "name"},
ListView: vw,
ReadView: vw,
EditView: vw,
}
schm := schema.New(
"name", field.String().WithUI(ui),
)
schm.UI = ui
j := `
---
ui:
widget: Widget
placeholder: Placeholder
options:
title: name
key: name
read_view:
widget: Widget
options:
title: name
key: name
edit_view:
widget: Widget
options:
title: name
key: name
list_view:
widget: Widget
options:
title: name
key: name
type: object
params:
inline: false
fields:
name:
ui:
widget: Widget
placeholder: Placeholder
options:
title: name
key: name
read_view:
widget: Widget
options:
title: name
key: name
edit_view:
widget: Widget
options:
title: name
key: name
list_view:
widget: Widget
options:
title: name
key: name
type: string
params: {}
loaded: false
`
sch, err := schema.FromYAML(strings.NewReader(j))
require.NoError(t, err)
assert.Equal(t, sch, schm)
}
func TestSchema_GetField(t *testing.T) {
sch := schema.New(
......
......@@ -25,6 +25,14 @@ func (o EnumOpt) String() string {
return fmt.Sprintf("%s", o.Value)
}
func (o EnumOpt) ValueInt() int {
return int(o.Value.(float64))
}
func (o EnumOpt) ValueString() string {
return o.Value.(string)
}
type enum []EnumOpt
func Enum(opts ...EnumOpt) Validator {
......@@ -66,3 +74,12 @@ func (t enum) ValidateOption() error {
}
return nil
}
func GetEnum(f *field.Field) []EnumOpt {
if v, ok := f.Options["enum"]; ok {
if e, ok := v.(*enum); ok {
return *e
}
}
return nil
}
......@@ -51,6 +51,12 @@ func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsO
return c, nil
}
func AddMetadata(key, value string) CollectionsOption {
return func(c *CollectionConfig) {
c.collection.Schema.WithMetadata(key, value)
}
}
func SkipMigration() CollectionsOption {
return func(c *CollectionConfig) {
c.SkipMigration = true
......
package setup
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"
"git.perx.ru/perxis/perxis-go/pkg/roles"
"github.com/hashicorp/go-multierror"
)
type Config struct {
Roles []RoleConfig
Clients []ClientConfig
Collections []CollectionConfig
Items []ItemConfig
}
func NewConfig() *Config {
return &Config{}
}
// Load загружает Config из файловой системы
// Файлы должны быть расположены в директории со следующей структурой:
// - collections/ - директория с файлами конфигурации коллекций
// - clients/ - директория с файлами конфигурации клиентов
// - items/ - директория с файлами конфигурации элементов
// - roles/ - директория с файлами конфигурации ролей
func (cfg *Config) Load(fsys fs.FS) (*Config, error) {
if subFS, err := fs.Sub(fsys, "collections"); err == nil {
if _, err = cfg.LoadCollections(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
}
if subFS, err := fs.Sub(fsys, "items"); err == nil {
if _, err = cfg.LoadItems(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
}
if subFS, err := fs.Sub(fsys, "roles"); err == nil {
if _, err = cfg.LoadRoles(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
}
if subFS, err := fs.Sub(fsys, "clients"); err == nil {
if _, err = cfg.LoadClients(subFS); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
}
return cfg, nil
}
func (cfg *Config) MustLoad(fsys fs.FS) *Config {
c, err := cfg.Load(fsys)
if err != nil {
panic(err)
}
return c
}
func (cfg *Config) WithCollectionOptions(filter func(c *collections.Collection) bool, opts ...CollectionsOption) *Config {
for _, c := range cfg.Collections {
if filter(c.collection) {
for _, o := range opts {
o(&c)
}
}
}
return cfg
}
func (cfg *Config) WithItemsOptions(filter func(c *items.Item) bool, opts ...ItemsOption) *Config {
for _, i := range cfg.Items {
if filter(i.item) {
for _, o := range opts {
o(&i)
}
}
}
return cfg
}
func (cfg *Config) WithRolesOptions(filter func(c *roles.Role) bool, opts ...RolesOption) *Config {
for _, r := range cfg.Roles {
if filter(r.role) {
for _, o := range opts {
o(&r)
}
}
}
return cfg
}
func (cfg *Config) WithClientsOptions(filter func(c *clients.Client) bool, opts ...ClientsOption) *Config {
for _, c := range cfg.Clients {
if filter(c.client) {
for _, o := range opts {
o(&c)
}
}
}
return cfg
}
// LoadItems загружает элементы из указанной файловой системы
func (cfg *Config) LoadClients(fsys fs.FS, opt ...ClientsOption) (*Config, error) {
assets := perxis.NewAssets[*clients.Client]()
cls, err := assets.FromFS(fsys)
if err != nil {
return nil, err
}
return cfg.AddClients(cls, opt...), nil
}
func (cfg *Config) MustLoadClients(fsys fs.FS, opt ...ClientsOption) *Config {
c, err := cfg.LoadClients(fsys, opt...)
if err != nil {
panic(err)
}
return c
}
// AddClients добавляет требования к настройке приложений в пространстве
func (cfg *Config) AddClients(clients []*clients.Client, opt ...ClientsOption) *Config {
for _, client := range clients {
cfg.AddClient(client, opt...)
}
return cfg
}
// AddClient добавляет требования к настройке приложений в пространстве
func (c *Config) AddClient(client *clients.Client, opt ...ClientsOption) *Config {
c.Clients = append(c.Clients, NewClientConfig(client, opt...))
return c
}
// LoadItems загружает элементы из указанной файловой системы
func (cfg *Config) LoadRoles(fsys fs.FS, opt ...RolesOption) (*Config, error) {
assets := perxis.NewAssets[*roles.Role]()
rls, err := assets.FromFS(fsys)
if err != nil {
return nil, err
}
return cfg.AddRoles(rls, opt...), nil
}
func (cfg *Config) MustLoadRoles(fsys fs.FS, opt ...RolesOption) *Config {
c, err := cfg.LoadRoles(fsys, opt...)
if err != nil {
panic(err)
}
return c
}
// AddRoles добавляет требования к настройке ролей в пространстве
func (cfg *Config) AddRoles(roles []*roles.Role, opt ...RolesOption) *Config {
for _, role := range roles {
cfg.AddRole(role, opt...)
}
return cfg
}
// AddRole добавляет требования к настройке ролей в пространстве
func (cfg *Config) AddRole(role *roles.Role, opt ...RolesOption) *Config {
cfg.Roles = append(cfg.Roles, NewRoleConfig(role, opt...))
return cfg
}
// AddCollections добавляет требования к настройке коллекций в пространстве
func (cfg *Config) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) (*Config, error) {
var errs *multierror.Error
for _, col := range collections {
if _, err := cfg.AddCollection(col, opt...); err != nil {
errs = multierror.Append(errs, err)
}
}
return cfg, errs.ErrorOrNil()
}
// AddCollection добавляет требование к настройке коллекции в пространстве
func (cfg *Config) AddCollection(collection *collections.Collection, opt ...CollectionsOption) (*Config, error) {
config, err := NewCollectionConfig(collection, opt...)
if err != nil {
return nil, err
}
cfg.Collections = append(cfg.Collections, config)
return cfg, nil
}
// MustAddCollection добавляет требование к настройке коллекции в пространстве
func (cfg *Config) MustAddCollection(collection *collections.Collection, opt ...CollectionsOption) *Config {
config, err := NewCollectionConfig(collection, opt...)
if err != nil {
panic(err)
}
cfg.Collections = append(cfg.Collections, config)
return cfg
}
// LoadCollections загружает коллекции из указанной файловой системы
func (cfg *Config) LoadCollections(fsys fs.FS, opt ...CollectionsOption) (*Config, error) {
colls, err := collections.FromFS(fsys)
if err != nil {
return nil, err
}
return cfg.AddCollections(colls, opt...)
}
func (cfg *Config) MustLoadCollections(fsys fs.FS, opt ...CollectionsOption) *Config {
c, err := cfg.LoadCollections(fsys, opt...)
if err != nil {
panic(err)
}
return c
}
// GetCollection возвращает коллекцию по идентификатору
func (cfg *Config) GetCollection(id string) *collections.Collection {
for _, c := range cfg.Collections {
if c.collection.ID == id {
return c.collection
}
}
return nil
}
// GetCollection возвращает коллекцию по идентификатору
func (cfg *Config) GetAllCollections() []*collections.Collection {
res := make([]*collections.Collection, 0, len(cfg.Collections))
for _, c := range cfg.Collections {
res = append(res, c.collection)
}
return res
}
// GetCollectionConfig возвращает конфигурацию коллекции по идентификатору
func (cfg *Config) GetCollectionConfig(id string) *CollectionConfig {
for _, c := range cfg.Collections {
if c.collection.ID == id {
return &c
}
}
return nil
}
// LoadItems загружает элементы из указанной файловой системы
func (cfg *Config) LoadItems(fsys fs.FS, opt ...ItemsOption) (*Config, error) {
assets := perxis.NewAssets[*items.Item]()
itms, err := assets.FromFS(fsys)
if err != nil {
return nil, err
}
return cfg.AddItems(itms, append(opt, DecodeItem())...), nil
}
func (cfg *Config) MustLoadItems(fsys fs.FS, opt ...ItemsOption) *Config {
cfg, err := cfg.LoadItems(fsys, opt...)
if err != nil {
panic(err)
}
return cfg
}
// AddItems добавляет требования к настройке элементов в пространстве
func (cfg *Config) AddItems(items []*items.Item, opt ...ItemsOption) *Config {
for _, item := range items {
cfg.AddItem(item, opt...)
}
return cfg
}
// AddItem добавляет требования к настройке элементов в пространстве
func (cfg *Config) AddItem(item *items.Item, opt ...ItemsOption) *Config {
cfg.Items = append(cfg.Items, NewItemConfig(item, opt...))
return cfg
}
// GetItems возвращает элементы для указанной коллекции
func (cfg *Config) GetItems(collectionId string) []*items.Item {
var items []*items.Item
for _, i := range cfg.Items {
if i.item.CollectionID == collectionId {
items = append(items, i.item)
}
}
return items
}
// GetItem возвращает элемент для указанной коллекции и идентификатора
func (cfg *Config) GetItem(collectionId, itemId string) *items.Item {
for _, i := range cfg.Items {
if i.item.CollectionID == collectionId && i.item.ID == itemId {
return i.item
}
}
return nil
}
......@@ -5,6 +5,7 @@ import (
"reflect"
"strings"
"git.perx.ru/perxis/perxis-go/pkg/collections"
"git.perx.ru/perxis/perxis-go/pkg/errors"
"git.perx.ru/perxis/perxis-go/pkg/items"
"go.uber.org/zap"
......@@ -27,6 +28,9 @@ type ItemConfig struct {
PublishFn PublishItemFn
UpdateFn UpdateItemFn
DeleteFn DeleteItemFn
// Если запись загружена из файла, необходимо выполнить Decode перед установкой
encoded bool
}
func NewItemConfig(item *items.Item, opt ...ItemsOption) ItemConfig {
......@@ -43,12 +47,24 @@ func NewItemConfig(item *items.Item, opt ...ItemsOption) ItemConfig {
return c
}
func PrepareItems(handler func(*items.Item)) ItemsOption {
return func(c *ItemConfig) {
handler(c.item)
}
}
func OverwriteItem() ItemsOption {
return func(c *ItemConfig) {
c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return new, true }
}
}
func DecodeItem() ItemsOption {
return func(c *ItemConfig) {
c.encoded = true
}
}
func OverwriteFields(fields ...string) ItemsOption {
return func(c *ItemConfig) {
c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) {
......@@ -135,15 +151,41 @@ func KeepDraft() ItemsOption {
}
}
func (s *Setup) InstallItems(ctx context.Context) error {
func (s *Setup) InstallItems(ctx context.Context) (err error) {
if len(s.Items) == 0 {
return nil
}
s.logger.Debug("Install items", zap.Int("Items", len(s.Items)))
for col, itms := range s.groupByCollection() {
exists, err := s.getExisting(ctx, col, itms)
for collID, itms := range s.groupByCollection() {
var coll *collections.Collection
for i, c := range itms {
if !c.encoded {
continue
}
if coll == nil {
coll, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, collID)
if err != nil {
return err
}
}
decoded, err := c.item.Decode(ctx, coll.Schema)
if err != nil {
return err
}
itms[i] = ItemConfig{
item: decoded,
PublishFn: c.PublishFn,
UpdateFn: c.UpdateFn,
DeleteFn: c.DeleteFn,
encoded: false,
}
}
exists, err := s.getExisting(ctx, collID, itms)
if err != nil {
return err
}
......
......@@ -2,6 +2,7 @@ package setup
import (
"context"
"io/fs"
"git.perx.ru/perxis/perxis-go/pkg/clients"
"git.perx.ru/perxis/perxis-go/pkg/collections"
......@@ -23,11 +24,6 @@ type Setup struct {
SpaceID string
EnvironmentID string
Roles []RoleConfig
Clients []ClientConfig
Collections []CollectionConfig
Items []ItemConfig
content *content.Content
force bool
......@@ -39,6 +35,8 @@ type Setup struct {
errors []error
logger *zap.Logger
*Config
}
func NewSetup(content *content.Content, spaceID, environmentID string, logger *zap.Logger) *Setup {
......@@ -54,9 +52,16 @@ func NewSetup(content *content.Content, spaceID, environmentID string, logger *z
content: content,
logger: logger,
waitSpaceAvailable: true,
Config: NewConfig(),
}
}
func (s *Setup) WithConfig(cfg *Config) *Setup {
setup := *s
setup.Config = cfg
return &setup
}
func (s *Setup) WithForce(force bool) *Setup {
setup := *s
setup.force = force
......@@ -99,63 +104,6 @@ func (s *Setup) Error() error {
return errors.WithErrors(ErrInvalidSetupConfig, s.errors...)
}
// AddRoles добавляет требования к настройке ролей в пространстве
func (s *Setup) AddRoles(roles []*roles.Role, opt ...RolesOption) *Setup {
for _, role := range roles {
s.AddRole(role, opt...)
}
return s
}
func (s *Setup) AddRole(role *roles.Role, opt ...RolesOption) *Setup {
s.Roles = append(s.Roles, NewRoleConfig(role, opt...))
return s
}
// AddClients добавляет требования к настройке приложений в пространстве
func (s *Setup) AddClients(clients []*clients.Client, opt ...ClientsOption) *Setup {
for _, client := range clients {
s.AddClient(client, opt...)
}
return s
}
func (s *Setup) AddClient(client *clients.Client, opt ...ClientsOption) *Setup {
s.Clients = append(s.Clients, NewClientConfig(client, opt...))
return s
}
// AddCollections добавляет требования к настройке коллекций в пространстве
func (s *Setup) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) *Setup {
for _, col := range collections {
s.AddCollection(col, opt...)
}
return s
}
func (s *Setup) AddCollection(collection *collections.Collection, opt ...CollectionsOption) *Setup {
config, err := NewCollectionConfig(collection, opt...)
if err != nil {
s.AddError(err)
return s
}
s.Collections = append(s.Collections, config)
return s
}
// AddItems добавляет требования к настройке элементов в пространстве
func (s *Setup) AddItems(items []*items.Item, opt ...ItemsOption) *Setup {
for _, item := range items {
s.AddItem(item, opt...)
}
return s
}
func (s *Setup) AddItem(item *items.Item, opt ...ItemsOption) *Setup {
s.Items = append(s.Items, NewItemConfig(item, opt...))
return s
}
// Install выполняет установку необходимых требований
func (s *Setup) Install(ctx context.Context) error {
if s.waitSpaceAvailable {
......@@ -222,3 +170,73 @@ func (s *Setup) Uninstall(ctx context.Context) error {
}
return nil
}
// Deprecated: use Config
func (s *Setup) Load(fsys fs.FS) *Setup {
var err error
s.Config, err = s.Config.Load(fsys)
if err != nil {
s.AddError(err)
}
return s
}
// Deprecated: use Config instead
// AddCollections добавляет требования к настройке коллекций в пространстве
func (s *Setup) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) *Setup {
for _, col := range collections {
s.AddCollection(col, opt...)
}
return s
}
// Deprecated: use Config instead
// AddCollection добавляет требование к настройке коллекции в пространстве
func (s *Setup) AddCollection(collection *collections.Collection, opt ...CollectionsOption) *Setup {
if _, err := s.Config.AddCollection(collection, opt...); err != nil {
s.AddError(err)
}
return s
}
// Deprecated: use Config instead
// AddRoles добавляет требования к настройке ролей в пространстве
func (s *Setup) AddRoles(roles []*roles.Role, opt ...RolesOption) *Setup {
s.Config = s.Config.AddRoles(roles, opt...)
return s
}
// Deprecated: use Config instead
// AddRole добавляет требования к настройке элементов в пространстве
func (s *Setup) AddRole(role *roles.Role, opt ...RolesOption) *Setup {
s.Config = s.Config.AddRole(role, opt...)
return s
}
// Deprecated: use Config instead
// AddItem добавляет требования к настройке элементов в пространстве
func (s *Setup) AddItem(item *items.Item, opt ...ItemsOption) *Setup {
s.Config = s.Config.AddItem(item, opt...)
return s
}
// Deprecated: use Config instead
// AddItems добавляет требования к настройке элементов в пространстве
func (s *Setup) AddItems(items []*items.Item, opt ...ItemsOption) *Setup {
s.Config = s.Config.AddItems(items, opt...)
return s
}
// Deprecated: use Config instead
// AddClient добавляет требования к настройке элементов в пространстве
func (s *Setup) AddClient(client *clients.Client, opt ...ClientsOption) *Setup {
s.Config = s.Config.AddClient(client, opt...)
return s
}
// Deprecated: use Config instead
// AddClients добавляет требования к настройке элементов в пространстве
func (s *Setup) AddClients(clients []*clients.Client, opt ...ClientsOption) *Setup {
s.Config = s.Config.AddClients(clients, opt...)
return s
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment