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/assets/tests/invalid.txt b/assets/tests/invalid.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/assets/tests/item.json b/assets/tests/item.json new file mode 100644 index 0000000000000000000000000000000000000000..1240b61e1efb8370c3000cafd213a6dfe8de7d53 --- /dev/null +++ b/assets/tests/item.json @@ -0,0 +1,16 @@ +{ + "id": "item3", + "enum": 1, + "data": { + "obj": { + "str": "value" + }, + "arr": [ + "str1", + "str2" + ] + }, + "struct": { + "option": true + } +} \ No newline at end of file diff --git a/assets/tests/items.yaml b/assets/tests/items.yaml new file mode 100644 index 0000000000000000000000000000000000000000..eacf244f47504be87023f538cdd7f02cdbbe7ea0 --- /dev/null +++ b/assets/tests/items.yaml @@ -0,0 +1,23 @@ +--- +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 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/collections/collection.go b/pkg/collections/collection.go index 6583b4239912413dce66e4e4685fbe4868411e2a..51cc3072e4b8123835bf9a5ceb5c0c5070cc8482 100644 --- a/pkg/collections/collection.go +++ b/pkg/collections/collection.go @@ -1,6 +1,7 @@ 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 +} diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index c5c1dd4cd32987fc0649d1fd8278e93fe794530a..ee87e981e6d31bb39b52516870d3bc637182a5af 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -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...) -} diff --git a/pkg/extension/service/extension.go b/pkg/extension/service/extension.go index d64590e39211619465bdca8839b9e28f4e678f23..338dadc29ff89a46d3e054ce47c202ff7707f549 100644 --- a/pkg/extension/service/extension.go +++ b/pkg/extension/service/extension.go @@ -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() diff --git a/pkg/files/field.go b/pkg/files/field.go index 5ce4dc4ce72506fdd62e184b95487319f95e5122..00659012aac439a835332ae9320fa64f2a03f9f9 100644 --- a/pkg/files/field.go +++ b/pkg/files/field.go @@ -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 } diff --git a/pkg/files/file.go b/pkg/files/file.go index 149d88a450bb5888c60f52441610f5be453d1bd7..ce49d8e227cd8a71dbd2fd948fb9c4e8cecc382f 100644 --- a/pkg/files/file.go +++ b/pkg/files/file.go @@ -3,7 +3,7 @@ package files import ( "bytes" "fmt" - "io/fs" + "io" "strings" "text/template" @@ -16,13 +16,13 @@ const ( // File - описание файла РІ системе хранения perxis type File struct { - ID string `mapstructure:"id,omitempty" json:"id" expr:"id"` // Уникальный идентификатор файла РІ хранилище - Name string `mapstructure:"name,omitempty" json:"name" bson:"name,omitempty" expr:"name"` // РРјСЏ файла - Size uint64 `mapstructure:"size,omitempty" json:"size" bson:"size,omitempty" expr:"size"` // Размер файла - 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:"-"` // Файл для загрузки(РёР· файловой системы) + ID string `mapstructure:"id,omitempty" json:"id" expr:"id"` // Уникальный идентификатор файла РІ хранилище + Name string `mapstructure:"name,omitempty" json:"name" bson:"name,omitempty" expr:"name"` // РРјСЏ файла + Size uint64 `mapstructure:"size,omitempty" json:"size" bson:"size,omitempty" expr:"size"` // Размер файла + 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"` // Ключ для хранения файла РІ хранилище + Content io.Reader `mapstructure:"-" json:"-" bson:"-"` // Альтернативный СЃРїРѕСЃРѕР± задать содержимое файла } func (f File) Clone() *File { diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index bb91b4203cf6c7e44991a41f5bdc31294aeedb4d..71d872763e886ad91555717d7b1ba86804c58cc5 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -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) +} diff --git a/pkg/schema/schema_yaml.go b/pkg/schema/schema_yaml.go deleted file mode 100644 index 7bfe6365ca648649ce353e8534aead2d4f2f2419..0000000000000000000000000000000000000000 --- a/pkg/schema/schema_yaml.go +++ /dev/null @@ -1,55 +0,0 @@ -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) -} diff --git a/pkg/schema/test/convert_test.go b/pkg/schema/test/convert_test.go index 1f6149ce375f4f4863483c8392e849a85db6e0f8..170bee8320b510244923d9c3905c20c17dc1d3f7 100644 --- a/pkg/schema/test/convert_test.go +++ b/pkg/schema/test/convert_test.go @@ -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хемы должны соответствовать объекту РёР· функции") }) } diff --git a/pkg/schema/test/object_test.go b/pkg/schema/test/object_test.go index 6b53b0ad1993305bce4c6b3797b8c5fe5cffc01c..f17b64511668ad8b4a86c5d91cb908a781384493 100644 --- a/pkg/schema/test/object_test.go +++ b/pkg/schema/test/object_test.go @@ -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( diff --git a/pkg/schema/validate/enum.go b/pkg/schema/validate/enum.go index 76d2ecc698792346e1f9dd60940e47d050593b96..8df38c6d68f579f468f0f088170368a31b8f3c80 100644 --- a/pkg/schema/validate/enum.go +++ b/pkg/schema/validate/enum.go @@ -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 +} diff --git a/pkg/setup/collection.go b/pkg/setup/collection.go index 9f5f6a46c7818d54db7e5123cdbef595b0cc0ab9..2d3656eaebd339dd9e064cef57e2ff6e501b1e2b 100644 --- a/pkg/setup/collection.go +++ b/pkg/setup/collection.go @@ -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 diff --git a/pkg/setup/config.go b/pkg/setup/config.go new file mode 100644 index 0000000000000000000000000000000000000000..08e1c740323d4e709e0a2d905ed2b2af2d4ebc5b --- /dev/null +++ b/pkg/setup/config.go @@ -0,0 +1,305 @@ +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 +} diff --git a/pkg/setup/item.go b/pkg/setup/item.go index a17512d0c0ce38714cc52cd7752ec7149ae292b3..af08ea951ad51da0a7cb7057191288d4997faea4 100644 --- a/pkg/setup/item.go +++ b/pkg/setup/item.go @@ -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 } diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go index b087337fcb57e3cb1a76cad1e3354291b944bdb0..9b6473a556e063518a78bf4e30ac1f844c658991 100644 --- a/pkg/setup/setup.go +++ b/pkg/setup/setup.go @@ -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 +}