From 3947c64ff89db1a9d2dfb54d01149bc2c132d273 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 14 Jun 2023 23:15:18 +0800 Subject: [PATCH] [Feature] Support custom widget (refactor widget) --- engine/load.go | 18 ++- widget/driver/connector.go | 177 +++++++++++++++++++++++++ widget/driver/source.go | 82 ++++++++++++ widget/instance.go | 57 +++++++++ widget/load.go | 108 ++++++++++++++++ widget/load_test.go | 29 +++++ widget/process.go | 50 ++++++++ widget/process_test.go | 51 ++++++++ widget/types.go | 53 ++++++++ widget/widget.go | 255 ++++++++++++++++++++++++------------- widget/widget_test.go | 231 +++++++++++++++++++++++++++++++-- 11 files changed, 1006 insertions(+), 105 deletions(-) create mode 100644 widget/driver/connector.go create mode 100644 widget/driver/source.go create mode 100644 widget/instance.go create mode 100644 widget/load.go create mode 100644 widget/load_test.go create mode 100644 widget/process.go create mode 100644 widget/process_test.go create mode 100644 widget/types.go diff --git a/engine/load.go b/engine/load.go index dde7c14b..bffcd4ff 100644 --- a/engine/load.go +++ b/engine/load.go @@ -172,12 +172,6 @@ func Load(cfg config.Config) (err error) { printErr(cfg.Mode, "Schedule", err) } - // Load Custom Widget - err = widget.Load(cfg) - if err != nil { - printErr(cfg.Mode, "Widget", err) - } - // Load AIGC err = aigc.Load(cfg) if err != nil { @@ -190,6 +184,18 @@ func Load(cfg config.Config) (err error) { printErr(cfg.Mode, "AIGC", err) } + // Load Custom Widget + err = widget.Load(cfg) + if err != nil { + printErr(cfg.Mode, "Widget", err) + } + + // Load Custom Widget Instances + err = widget.LoadInstances() + if err != nil { + printErr(cfg.Mode, "Widget", err) + } + return nil } diff --git a/widget/driver/connector.go b/widget/driver/connector.go new file mode 100644 index 00000000..1c02e42d --- /dev/null +++ b/widget/driver/connector.go @@ -0,0 +1,177 @@ +package driver + +import ( + "fmt" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/application" + "github.com/yaoapp/gou/connector" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/xun/capsule" + "github.com/yaoapp/xun/dbal/query" + "github.com/yaoapp/xun/dbal/schema" + "github.com/yaoapp/yao/share" +) + +// Connector the store driver +type Connector struct { + Connector string + Table string + Reload bool + Widget string + query query.Query + schema schema.Schema +} + +// NewConnector create a new stroe driver +func NewConnector(widgetID string, connectorName string, tableName string, reload bool) (*Connector, error) { + if connectorName == "" { + connectorName = "default" + } + + if tableName == "" { + tableName = fmt.Sprintf("__yao_dsl_%s", widgetID) + } + + store := &Connector{Widget: widgetID, Connector: connectorName, Reload: reload, Table: tableName} + if store.Connector == "default" { + store.query = capsule.Global.Query() + store.schema = capsule.Global.Schema() + + } else { + + conn, err := connector.Select(connectorName) + if err != nil { + return nil, err + } + + if !conn.Is(connector.DATABASE) { + return nil, fmt.Errorf("The connector %s is not a database connector", connectorName) + } + + store.query, err = conn.Query() + if err != nil { + return nil, err + } + + store.schema, err = conn.Schema() + if err != nil { + return nil, err + } + } + + err := store.init() + if err != nil { + return nil, err + } + + return store, nil +} + +// Walk load the widget instances +func (app *Connector) Walk(cb func(string, map[string]interface{})) error { + + rows, err := app.query. + Table(app.Table). + Select("file", "source"). + Limit(5000). + Get() + + if err != nil { + return err + } + + messages := []string{} + for _, row := range rows { + + source := map[string]interface{}{} + data := []byte(row["source"].(string)) + file := row["file"].(string) + + id := share.ID("", file) + err := application.Parse(row["file"].(string), data, &source) + if err != nil { + messages = append(messages, err.Error()) + continue + } + cb(id, source) + } + + if len(messages) > 0 { + return fmt.Errorf("%s", strings.Join(messages, ";\n")) + } + + return nil +} + +// Save save the widget DSL +func (app *Connector) Save(file string, source map[string]interface{}) error { + + bytes, err := jsoniter.Marshal(source) + if err != nil { + return err + } + + content := string(bytes) + + has, err := app.query.Table(app.Table).Where("file", file).Exists() + if err != nil { + return err + } + + if has { + _, err = app.query.Table(app.Table).Where("file", file).Update(map[string]interface{}{"source": content}) + } else { + err = app.query.Table(app.Table).Insert(map[string]interface{}{"file": file, "source": content}) + } + + return err +} + +// Remove remove the widget DSL +func (app *Connector) Remove(file string) error { + _, err := app.query.Table(app.Table).Where("file", file).Delete() + return err +} + +// init the widget store +func (app *Connector) init() error { + + has, err := app.schema.HasTable(app.Table) + if err != nil { + return err + } + + // create the table + if !has { + err = app.schema.CreateTable(app.Table, func(table schema.Blueprint) { + table.ID("id") // The ID field + table.String("file", 255).Unique() // The file name + table.Text("source").Null() + table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index() + table.TimestampTz("updated_at").Null().Index() + table.TimestampTz("expired_at").Null().Index() + }) + + if err != nil { + return err + } + log.Trace("Create the conversation table: %s", app.Table) + } + + // validate the table + tab, err := app.schema.GetTable(app.Table) + if err != nil { + return err + } + + fields := []string{"id", "file", "source", "created_at", "updated_at", "expired_at"} + for _, field := range fields { + if !tab.HasColumn(field) { + return fmt.Errorf("%s is required", field) + } + } + + return nil +} diff --git a/widget/driver/source.go b/widget/driver/source.go new file mode 100644 index 00000000..a9b0cea7 --- /dev/null +++ b/widget/driver/source.go @@ -0,0 +1,82 @@ +package driver + +import ( + "fmt" + "strings" + "sync" + + "github.com/yaoapp/gou/application" + "github.com/yaoapp/yao/share" +) + +// Source the application source driver +type Source struct { + Path string + Extensions []string + Instances sync.Map +} + +// NewSource create a new local driver +func NewSource(path string, exts []string) *Source { + return &Source{ + Path: path, + Extensions: exts, + } +} + +// Walk load the widget instances +func (app *Source) Walk(cb func(string, map[string]interface{})) error { + + if app.Path == "" { + return fmt.Errorf("The widget path is empty") + } + + if app.Extensions == nil || len(app.Extensions) == 0 { + app.Extensions = []string{"*.yao", "*.json", "*.jsonc"} + } + + messages := []string{} + err := application.App.Walk(app.Path, func(root, file string, isdir bool) error { + if isdir { + return nil + } + + id := share.ID(root, file) + source := map[string]interface{}{} + + data, err := application.App.Read(file) + if err != nil { + messages = append(messages, err.Error()) + return nil + } + + err = application.Parse(file, data, &source) + if err != nil { + messages = append(messages, err.Error()) + return nil + } + + cb(id, source) + return nil + }, app.Extensions...) + + if err != nil { + return err + } + + if len(messages) > 0 { + return fmt.Errorf("%s", strings.Join(messages, ";\n")) + } + + return nil +} + +// Save save the widget DSL +func (app *Source) Save(file string, source map[string]interface{}) error { + return fmt.Errorf("The widget source driver is read-only, using Studio API instead") +} + +// Remove remove the widget DSL +func (app *Source) Remove(file string) error { + return fmt.Errorf("The widget source driver is read-only, using Studio API instead") +} diff --git a/widget/instance.go b/widget/instance.go new file mode 100644 index 00000000..f31266ff --- /dev/null +++ b/widget/instance.go @@ -0,0 +1,57 @@ +package widget + +import ( + "github.com/yaoapp/gou/process" +) + +// NewInstance create a new widget instance +func NewInstance(widgetID string, instanceID string, source map[string]interface{}, loader LoaderDSL) *Instance { + return &Instance{id: instanceID, source: source, widget: widgetID, loader: loader} +} + +// Load load the widget instance +func (instance *Instance) Load() error { + if instance.loader.Load == "" { + return nil + } + dsl, err := instance.exec(instance.loader.Load, instance.id, instance.source) + if err != nil { + return err + } + + instance.dsl = dsl + return nil +} + +// Reload reload the widget instance +func (instance *Instance) Reload() error { + if instance.loader.Reload == "" { + return nil + } + + dsl, err := instance.exec(instance.loader.Reload, instance.id, instance.source, instance.dsl) + if err != nil { + return err + } + + instance.dsl = dsl + return nil +} + +// Unload unload the widget instance +func (instance *Instance) Unload() error { + if instance.loader.Unload == "" { + return nil + } + _, err := instance.exec(instance.loader.Unload, instance.id) + return err +} + +// exec exec the widget process +func (instance *Instance) exec(processName string, args ...interface{}) (interface{}, error) { + p, err := process.Of(processName, args...) + if err != nil { + return nil, err + } + return p.Exec() +} diff --git a/widget/load.go b/widget/load.go new file mode 100644 index 00000000..5f4fd229 --- /dev/null +++ b/widget/load.go @@ -0,0 +1,108 @@ +package widget + +import ( + "fmt" + "strings" + "sync" + + "github.com/yaoapp/gou/application" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widget/driver" +) + +// Widgets the loaded widgets +var Widgets = map[string]*DSL{} + +// Load Widgets +func Load(cfg config.Config) error { + + exts := []string{"*.wid.yao", "*.wid.json", "*.wid.jsonc"} + messages := []string{} + + err := application.App.Walk("widgets", func(root, file string, isdir bool) error { + if isdir { + return nil + } + + id := share.ID(root, file) + _, err := LoadFile(file, id) + if err != nil { + messages = append(messages, err.Error()) + } + return nil + }, exts...) + + if err != nil { + return err + } + + if len(messages) > 0 { + return fmt.Errorf("%s", strings.Join(messages, ";\n")) + } + + return nil +} + +// LoadInstances load widget instances +func LoadInstances() error { + messages := []string{} + for _, widget := range Widgets { + err := widget.LoadInstances() + if err != nil { + messages = append(messages, err.Error()) + } + } + + if len(messages) > 0 { + return fmt.Errorf("%s", strings.Join(messages, ";\n")) + } + return nil +} + +// LoadFile load widget by file +func LoadFile(file string, id string) (*DSL, error) { + + data, err := application.App.Read(file) + if err != nil { + return nil, err + } + + return LoadSource(data, file, id) +} + +// LoadSource load widget by source +func LoadSource(data []byte, file, id string) (*DSL, error) { + + widget := &DSL{ID: id, File: file, Instances: sync.Map{}} + err := application.Parse(file, data, &widget) + if err != nil { + return nil, err + } + + if widget.Remote != nil { + + widget.FS, err = driver.NewConnector(widget.ID, widget.Remote.Connector, widget.Remote.Table, widget.Remote.Reload) + if err != nil { + return nil, err + } + + } else { + widget.FS = driver.NewSource(widget.Path, widget.Extensions) + } + + // register the widget process + err = widget.RegisterProcess() + if err != nil { + return nil, err + } + + // register the widget api + err = widget.RegisterAPI() + if err != nil { + return nil, err + } + + Widgets[id] = widget + return Widgets[id], nil +} diff --git a/widget/load_test.go b/widget/load_test.go new file mode 100644 index 00000000..e845c205 --- /dev/null +++ b/widget/load_test.go @@ -0,0 +1,29 @@ +package widget + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou/api" + "github.com/yaoapp/gou/process" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/test" +) + +func TestLoad(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + Load(config.Conf) + check(t) +} + +func check(t *testing.T) { + assert.NotNil(t, Widgets["dyform"]) + assert.NotNil(t, api.APIs["__yao.widget.dyform"]) + assert.NotNil(t, process.Handlers["widgets.dyform.find"]) + assert.NotNil(t, process.Handlers["widgets.dyform.delete"]) + assert.NotNil(t, process.Handlers["widgets.dyform.cancel"]) + assert.NotNil(t, process.Handlers["widgets.dyform.save"]) + assert.NotNil(t, process.Handlers["widgets.dyform.setting"]) +} diff --git a/widget/process.go b/widget/process.go new file mode 100644 index 00000000..3d913f03 --- /dev/null +++ b/widget/process.go @@ -0,0 +1,50 @@ +package widget + +import ( + "github.com/yaoapp/gou/process" + "github.com/yaoapp/kun/exception" +) + +func init() { + process.RegisterGroup("widget", map[string]process.Handler{ + "Save": ProcessSave, + "Remove": ProcessRemove, + }) +} + +// ProcessSave process the widget save +func ProcessSave(process *process.Process) interface{} { + process.ValidateArgNums(3) + name := process.ArgsString(0) + file := process.ArgsString(1) + source := process.ArgsMap(2) + + widget, ok := Widgets[name] + if !ok { + exception.New("The widget %s not found", 404, name).Throw() + } + + err := widget.Save(file, source) + if err != nil { + exception.New(err.Error(), 500, name, err).Throw() + } + + return nil +} + +// ProcessRemove process the widget save +func ProcessRemove(process *process.Process) interface{} { + process.ValidateArgNums(2) + name := process.ArgsString(0) + file := process.ArgsString(1) + widget, ok := Widgets[name] + if !ok { + exception.New("The widget %s not found", 404, name).Throw() + } + + err := widget.Remove(file) + if err != nil { + exception.New(err.Error(), 500, name).Throw() + } + return nil +} diff --git a/widget/process_test.go b/widget/process_test.go new file mode 100644 index 00000000..e3d9177c --- /dev/null +++ b/widget/process_test.go @@ -0,0 +1,51 @@ +package widget + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou/process" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/test" +) + +func TestProcessSave(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + iform := preare(t)[1] + + assert.Panics(t, func() { + process.New("widget.Save", "dyform", "feedback/new.form.yao", map[string]interface{}{}).Run() + }) + + assert.NotPanics(t, func() { + process.New("widget.Save", "iform", "feedback/new.form.yao", map[string]interface{}{"columns": []interface{}{}}).Run() + }) + + defer iform.Remove("feedback/new.form.yao") + + instance, ok := iform.Instances.Load("feedback.new") + if !ok { + t.Fatal("feedback instance not found") + } + assert.Equal(t, "feedback.new", instance.(*Instance).id) +} + +func TestProcessRemove(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + iform := preare(t)[1] + + assert.Panics(t, func() { + process.New("widget.Remove", "dyform", "feedback/new.form.yao").Run() + }) + + assert.NotPanics(t, func() { + process.New("widget.Remove", "iform", "feedback/new.form.yao").Run() + }) + + defer iform.Remove("feedback/new.form.yao") + + _, ok := iform.Instances.Load("feedback.new") + assert.False(t, ok) +} diff --git a/widget/types.go b/widget/types.go new file mode 100644 index 00000000..2b2a8edb --- /dev/null +++ b/widget/types.go @@ -0,0 +1,53 @@ +package widget + +import ( + "sync" + + "github.com/yaoapp/gou/api" +) + +// DSL is the widget DSL +type DSL struct { + ID string `json:"-"` + File string `json:"-"` + Instances sync.Map `json:"-"` + FS FS `json:"-"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Path string `json:"path,omitempty"` + Extensions []string `json:"extensions,omitempty"` + Remote *RemoteDSL `json:"remote,omitempty"` + Loader LoaderDSL `json:"loader"` + Process map[string]string `json:"process,omitempty"` + API *api.HTTP `json:"api,omitempty"` +} + +// RemoteDSL is the remote widget DSL +type RemoteDSL struct { + Connector string `json:"connector,omitempty"` + Table string `json:"table,omitempty"` + Reload bool `json:"reload,omitempty"` +} + +// LoaderDSL is the loader widget DSL +type LoaderDSL struct { + Load string `json:"load,omitempty"` + Reload string `json:"reload,omitempty"` + Unload string `json:"unload,omitempty"` +} + +// Instance is the widget instance +type Instance struct { + source map[string]interface{} + dsl interface{} + loader LoaderDSL + id string + widget string +} + +// FS is the DSL File system +type FS interface { + Walk(cb func(id string, source map[string]interface{})) error + Save(file string, source map[string]interface{}) error + Remove(file string) error +} diff --git a/widget/widget.go b/widget/widget.go index 7f780c11..8d2d0452 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -1,112 +1,189 @@ package widget import ( - "path/filepath" + "fmt" + "strings" - "github.com/yaoapp/gou/application" - "github.com/yaoapp/gou/widget" - "github.com/yaoapp/yao/config" + "github.com/yaoapp/gou/api" + "github.com/yaoapp/gou/process" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/yao/share" ) -// Load Widgets -func Load(cfg config.Config) error { +// LoadInstances load the widget instances +func (widget *DSL) LoadInstances() error { - register := moduleRegister() - return application.App.Walk("widgets", func(root, file string, isdir bool) error { - if isdir { - return nil + messages := []string{} + err := widget.FS.Walk(func(id string, source map[string]interface{}) { + instance := NewInstance(widget.ID, id, source, widget.Loader) + err := instance.Load() + if err != nil { + messages = append(messages, fmt.Sprintf("%v %s", id, err.Error())) + return } - path := filepath.Dir(file) - _, err := widget.Load(path, nil, register) - return err + widget.Instances.Store(id, instance) + }) - }, "widget.yao", "widget.json", "widget.jsonc") + if len(messages) > 0 { + return fmt.Errorf("widgets.%s Load: %s", widget.ID, strings.Join(messages, ";")) + } - // var root = filepath.Join(cfg.Root, "widgets") - // return LoadFrom(root) + return err } -// // LoadFrom widget -// func LoadFrom(dir string) error { +// ReloadInstances reload the widget instances +func (widget *DSL) ReloadInstances() error { -// register := moduleRegister() + messages := []string{} -// if share.DirNotExists(dir) { -// return fmt.Errorf("%s does not exists", dir) -// } + // Reload the remote widget + widget.Instances.Range(func(key, value interface{}) bool { + if instance, ok := value.(*Instance); ok { + err := instance.Reload() + if err != nil { + messages = append(messages, fmt.Sprintf("%v %s", key, err.Error())) + } + } + return true + }) -// paths, err := ioutil.ReadDir(dir) -// if err != nil { -// return err -// } + if len(messages) > 0 { + return fmt.Errorf("widgets.%s Reload: %s", widget.ID, strings.Join(messages, ";")) + } -// for _, path := range paths { + return nil +} -// if !path.IsDir() { -// continue -// } +// UnloadInstances unload the widget instances +func (widget *DSL) UnloadInstances() error { -// name := path.Name() -// if _, err := os.Stat(filepath.Join(dir, name, "widget.json")); errors.Is(err, os.ErrNotExist) { -// // path/to/whatever does not exist -// continue -// } -// w, err := gou.LoadWidget(filepath.Join(dir, name), name, register) -// if err != nil { -// return err -// } + messages := []string{} -// // Load instances -// err = w.Load() -// if err != nil { -// return err -// } -// } + // Unload the remote widget + widget.Instances.Range(func(key, value interface{}) bool { + if instance, ok := value.(*Instance); ok { + err := instance.Unload() + if err != nil { + messages = append(messages, fmt.Sprintf("%v %s", key, err.Error())) + } + widget.Instances.Delete(key) + } -// return err -// } + return true + }) -func moduleRegister() widget.ModuleRegister { - return widget.ModuleRegister{ - // "Apis": func(name string, source []byte) error { - // _, err := api.Load(string(source), name) - // log.Trace("[Widget] Register api %s", name) - // if err != nil { - // log.Error("[Widget] Register api %s %v", name, err) - // } - // return err - // }, - // "Models": func(name string, source []byte) error { - // _, err := model.Load(string(source), name) - // log.Trace("[Widget] Register model %s", name) - // if err != nil { - // log.Error("[Widget] Register model %s %v", name, err) - // } - // return err - // }, - // "Tables": func(name string, source []byte) error { - // log.Trace("[Widget] Register table %s", name) - // _, err := table.LoadTable(string(source), name) - // if err != nil { - // log.Error("[Widget] Register table %s %v", name, err) - // } - // return nil - // }, - // "Tasks": func(name string, source []byte) error { - // log.Trace("[Widget] Register task %s", name) - // _, err := gou.LoadTask(string(source), name) - // if err != nil { - // log.Error("[Widget] Register task %s %v", name, err) - // } - // return nil - // }, - // "Schedules": func(name string, source []byte) error { - // log.Trace("[Widget] Register schedule %s", name) - // _, err := gou.LoadSchedule(string(source), name) - // if err != nil { - // log.Error("[Widget] Register schedule %s %v", name, err) - // } - // return nil - // }, + if len(messages) > 0 { + return fmt.Errorf("widgets.%s Unload: %s", widget.ID, strings.Join(messages, ";")) + } + + return nil +} + +// RegisterProcess register the widget process +func (widget *DSL) RegisterProcess() error { + if widget.Process == nil { + return nil + } + + handlers := map[string]process.Handler{} + for name, processName := range widget.Process { + + if processName == "" { + continue + } + handlers[name] = widget.handler(processName) + } + + process.RegisterGroup(fmt.Sprintf("widgets.%s", widget.ID), handlers) + return nil +} + +// RegisterAPI register the widget API +func (widget *DSL) RegisterAPI() error { + + if widget.API == nil { + return nil + } + + id := fmt.Sprintf("__yao.widget.%s", widget.ID) + widget.API.Group = fmt.Sprintf("/__yao/widget/%s", widget.ID) + + // Register the widget API + api.APIs[id] = &api.API{ + ID: fmt.Sprintf("__yao.widget.%s", widget.ID), + File: widget.File, + HTTP: *widget.API, + Type: "http", + } + + return nil +} + +// Register the process handler +func (widget *DSL) handler(processName string) process.Handler { + + return func(p *process.Process) interface{} { + + p.ValidateArgNums(1) + instanceID := p.ArgsString(0) + instance, ok := widget.Instances.Load(instanceID) + if !ok { + exception.New("The widget %s instance %s not found", 404, widget.ID, instanceID).Throw() + } + + args := []interface{}{} + args = append(args, p.Args...) + args = append(args, instance.(*Instance).dsl) + + return process.New(processName, args...).Run() } } + +// Save the widget source to file +func (widget *DSL) Save(file string, source map[string]interface{}) error { + + err := widget.FS.Save(file, source) + if err != nil { + return err + } + + id := share.ID("", file) + instance := NewInstance(widget.ID, id, source, widget.Loader) + + // new instance + old, ok := widget.Instances.Load(id) + if !ok { + err := instance.Load() + if err != nil { + return err + } + + widget.Instances.Store(id, instance) + return nil + } + + // Reload the instance + if widget.Remote != nil && widget.Remote.Reload { + instance.dsl = old.(*Instance).dsl + err = instance.Reload() + if err != nil { + return err + } + } + + widget.Instances.Store(id, instance) + return nil +} + +// Remove the widget source file +func (widget *DSL) Remove(file string) error { + + err := widget.FS.Remove(file) + if err != nil { + return err + } + + id := share.ID("", file) + widget.Instances.Delete(id) + return nil +} diff --git a/widget/widget_test.go b/widget/widget_test.go index 81218369..6fa3432e 100644 --- a/widget/widget_test.go +++ b/widget/widget_test.go @@ -1,26 +1,237 @@ package widget import ( + "fmt" + "net/http" + "net/http/httptest" "testing" + "github.com/gin-gonic/gin" + jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" - "github.com/yaoapp/gou/widget" + "github.com/yaoapp/gou/api" + "github.com/yaoapp/gou/process" + "github.com/yaoapp/xun/capsule" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/test" ) -func TestLoad(t *testing.T) { +func TestWidgetLoadInstances(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() + for _, widget := range preare(t) { + err := widget.LoadInstances() + if err != nil { + t.Fatal(err) + } - Load(config.Conf) - check(t) -} + instance, ok := widget.Instances.Load("feedback") + if !ok { + t.Fatal("feedback instance not found") + } -func check(t *testing.T) { - ids := map[string]bool{} - for id := range widget.Widgets { - ids[id] = true + assert.Equal(t, "feedback", instance.(*Instance).id) + assert.Equal(t, "feedback", instance.(*Instance).dsl.(map[string]interface{})["id"]) } - assert.True(t, ids["dyform"]) +} + +func TestWidgetReLoadInstances(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + for _, widget := range preare(t) { + err := widget.LoadInstances() + if err != nil { + t.Fatal(err) + } + + instance, ok := widget.Instances.Load("feedback") + if !ok { + t.Fatal("feedback instance not found") + } + + err = widget.ReloadInstances() + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "feedback", instance.(*Instance).id) + assert.Equal(t, "feedback", instance.(*Instance).dsl.(map[string]interface{})["id"]) + assert.Equal(t, true, instance.(*Instance).dsl.(map[string]interface{})["tests.reload"]) + } +} + +func TestWidgetUnLoadInstances(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + for _, widget := range preare(t) { + err := widget.LoadInstances() + if err != nil { + t.Fatal(err) + } + + instance, ok := widget.Instances.Load("feedback") + if !ok { + t.Fatal("feedback instance not found") + } + + assert.Equal(t, "feedback", instance.(*Instance).id) + assert.Equal(t, "feedback", instance.(*Instance).dsl.(map[string]interface{})["id"]) + + err = widget.UnloadInstances() + if err != nil { + t.Fatal(err) + } + + _, ok = widget.Instances.Load("feedback") + assert.False(t, ok) + } +} + +func TestWidgetRegisterProcess(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + for _, widget := range preare(t) { + err := widget.LoadInstances() + if err != nil { + t.Fatal(err) + } + + name := fmt.Sprintf("widgets.%s.Setting", widget.ID) + res := process.New(name, "feedback").Run() + assert.Equal(t, "feedback", res.(map[string]interface{})["id"]) + assert.Equal(t, "feedback", res.(map[string]interface{})["tests.id"]) + } +} + +func TestWidgetRegisterAPI(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + for _, widget := range preare(t) { + err := widget.LoadInstances() + if err != nil { + t.Fatal(err) + } + + router := testRouter(t) + response := httptest.NewRecorder() + url := fmt.Sprintf("/api/__yao/widget/%s/feedback/setting", widget.ID) + req, _ := http.NewRequest("GET", url, nil) + router.ServeHTTP(response, req) + + res := map[string]interface{}{} + err = jsoniter.Unmarshal(response.Body.Bytes(), &res) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "feedback", res["id"]) + } +} + +func TestWidgetSaveCreate(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + dyform := preare(t)[0] + iform := preare(t)[1] + + err := dyform.Save("feedback/new.form.yao", map[string]interface{}{}) + assert.NotEmpty(t, err) + + err = iform.Save("feedback/new.form.yao", map[string]interface{}{"columns": []interface{}{}}) + if err != nil { + t.Fatal(err) + } + defer iform.Remove("feedback/new.form.yao") + + instance, ok := iform.Instances.Load("feedback.new") + if !ok { + t.Fatal("feedback instance not found") + } + assert.Equal(t, "feedback.new", instance.(*Instance).id) +} + +func TestWidgetSaveUpdate(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + iform := preare(t)[1] + + err := iform.Save("feedback/new.form.yao", map[string]interface{}{"columns": []interface{}{}}) + if err != nil { + t.Fatal(err) + } + defer iform.Remove("feedback/new.form.yao") + + err = iform.Save("feedback/new.form.yao", map[string]interface{}{"columns": []interface{}{}, "foo": "bar"}) + if err != nil { + t.Fatal(err) + } + + instance, ok := iform.Instances.Load("feedback.new") + if !ok { + t.Fatal("feedback instance not found") + } + + assert.Equal(t, "feedback.new", instance.(*Instance).id) + assert.Equal(t, "bar", instance.(*Instance).dsl.(map[string]interface{})["foo"]) + assert.Equal(t, true, instance.(*Instance).dsl.(map[string]interface{})["tests.reload"]) +} + +func preare(t *testing.T) []*DSL { + err := Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + qb := capsule.Global.Query() + qb.Table("dsl_iform").Insert(map[string]interface{}{ + "file": "feedback.iform.yao", + "source": `{ + "columns": [ + [ + { "type": "Title", "label": "Feedback Information" }, + { "type": "Input", "label": "Name" }, + { "type": "Input", "label": "Email" } + ], + [ + { "type": "Title", "label": "Feedback Details" }, + { "type": "Textarea", "label": "Message" }, + { "type": "Checkbox", "label": "Anonymous" } + ] + ], + "actions": { + "left": [ + { + "type": "api", + "text": "Submit Feedback", + "api": "/api/__yao/widget/dyform/save", + "isPrimary": true + } + ], + "right": [ + { + "type": "info", + "text": "Help", + "info": "Need assistance? Click here." + }, + { + "type": "api", + "text": "Cancel", + "process": "widget.dyform.Cancel" + } + ] + } + } + `, + }) + + return []*DSL{Widgets["dyform"], Widgets["iform"]} +} + +func testRouter(t *testing.T, middlewares ...gin.HandlerFunc) *gin.Engine { + router := gin.New() + gin.SetMode(gin.ReleaseMode) + router.Use(middlewares...) + api.SetGuards(map[string]gin.HandlerFunc{"bearer-jwt": func(ctx *gin.Context) {}}) + api.SetRoutes(router, "/api") + return router }