[Feature] Support custom widget (refactor widget)

This commit is contained in:
Max 2023-06-14 23:15:18 +08:00
parent 6cb73059b9
commit 3947c64ff8
11 changed files with 1006 additions and 105 deletions

View file

@ -172,12 +172,6 @@ func Load(cfg config.Config) (err error) {
printErr(cfg.Mode, "Schedule", err) printErr(cfg.Mode, "Schedule", err)
} }
// Load Custom Widget
err = widget.Load(cfg)
if err != nil {
printErr(cfg.Mode, "Widget", err)
}
// Load AIGC // Load AIGC
err = aigc.Load(cfg) err = aigc.Load(cfg)
if err != nil { if err != nil {
@ -190,6 +184,18 @@ func Load(cfg config.Config) (err error) {
printErr(cfg.Mode, "AIGC", err) 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 return nil
} }

177
widget/driver/connector.go Normal file
View file

@ -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
}

82
widget/driver/source.go Normal file
View file

@ -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")
}

57
widget/instance.go Normal file
View file

@ -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()
}

108
widget/load.go Normal file
View file

@ -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
}

29
widget/load_test.go Normal file
View file

@ -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"])
}

50
widget/process.go Normal file
View file

@ -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
}

51
widget/process_test.go Normal file
View file

@ -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)
}

53
widget/types.go Normal file
View file

@ -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
}

View file

@ -1,112 +1,189 @@
package widget package widget
import ( import (
"path/filepath" "fmt"
"strings"
"github.com/yaoapp/gou/application" "github.com/yaoapp/gou/api"
"github.com/yaoapp/gou/widget" "github.com/yaoapp/gou/process"
"github.com/yaoapp/yao/config" "github.com/yaoapp/kun/exception"
"github.com/yaoapp/yao/share"
) )
// Load Widgets // LoadInstances load the widget instances
func Load(cfg config.Config) error { func (widget *DSL) LoadInstances() error {
register := moduleRegister() messages := []string{}
return application.App.Walk("widgets", func(root, file string, isdir bool) error { err := widget.FS.Walk(func(id string, source map[string]interface{}) {
if isdir { instance := NewInstance(widget.ID, id, source, widget.Loader)
return nil err := instance.Load()
if err != nil {
messages = append(messages, fmt.Sprintf("%v %s", id, err.Error()))
return
} }
path := filepath.Dir(file) widget.Instances.Store(id, instance)
_, err := widget.Load(path, nil, register) })
return err
}, "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 err
// return LoadFrom(root)
} }
// // LoadFrom widget // ReloadInstances reload the widget instances
// func LoadFrom(dir string) error { func (widget *DSL) ReloadInstances() error {
// register := moduleRegister() messages := []string{}
// if share.DirNotExists(dir) { // Reload the remote widget
// return fmt.Errorf("%s does not exists", dir) 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 len(messages) > 0 {
// if err != nil { return fmt.Errorf("widgets.%s Reload: %s", widget.ID, strings.Join(messages, ";"))
// return err }
// }
// for _, path := range paths { return nil
}
// if !path.IsDir() { // UnloadInstances unload the widget instances
// continue func (widget *DSL) UnloadInstances() error {
// }
// name := path.Name() messages := []string{}
// 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
// }
// // Load instances // Unload the remote widget
// err = w.Load() widget.Instances.Range(func(key, value interface{}) bool {
// if err != nil { if instance, ok := value.(*Instance); ok {
// return err 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 { if len(messages) > 0 {
return widget.ModuleRegister{ return fmt.Errorf("widgets.%s Unload: %s", widget.ID, strings.Join(messages, ";"))
// "Apis": func(name string, source []byte) error { }
// _, err := api.Load(string(source), name)
// log.Trace("[Widget] Register api %s", name) return nil
// if err != nil { }
// log.Error("[Widget] Register api %s %v", name, err)
// } // RegisterProcess register the widget process
// return err func (widget *DSL) RegisterProcess() error {
// }, if widget.Process == nil {
// "Models": func(name string, source []byte) error { return nil
// _, err := model.Load(string(source), name) }
// log.Trace("[Widget] Register model %s", name)
// if err != nil { handlers := map[string]process.Handler{}
// log.Error("[Widget] Register model %s %v", name, err) for name, processName := range widget.Process {
// }
// return err if processName == "" {
// }, continue
// "Tables": func(name string, source []byte) error { }
// log.Trace("[Widget] Register table %s", name) handlers[name] = widget.handler(processName)
// _, err := table.LoadTable(string(source), name) }
// if err != nil {
// log.Error("[Widget] Register table %s %v", name, err) process.RegisterGroup(fmt.Sprintf("widgets.%s", widget.ID), handlers)
// } return nil
// return nil }
// },
// "Tasks": func(name string, source []byte) error { // RegisterAPI register the widget API
// log.Trace("[Widget] Register task %s", name) func (widget *DSL) RegisterAPI() error {
// _, err := gou.LoadTask(string(source), name)
// if err != nil { if widget.API == nil {
// log.Error("[Widget] Register task %s %v", name, err) return nil
// } }
// return nil
// }, id := fmt.Sprintf("__yao.widget.%s", widget.ID)
// "Schedules": func(name string, source []byte) error { widget.API.Group = fmt.Sprintf("/__yao/widget/%s", widget.ID)
// log.Trace("[Widget] Register schedule %s", name)
// _, err := gou.LoadSchedule(string(source), name) // Register the widget API
// if err != nil { api.APIs[id] = &api.API{
// log.Error("[Widget] Register schedule %s %v", name, err) ID: fmt.Sprintf("__yao.widget.%s", widget.ID),
// } File: widget.File,
// return nil 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
}

View file

@ -1,26 +1,237 @@
package widget package widget
import ( import (
"fmt"
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert" "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/config"
"github.com/yaoapp/yao/test" "github.com/yaoapp/yao/test"
) )
func TestLoad(t *testing.T) { func TestWidgetLoadInstances(t *testing.T) {
test.Prepare(t, config.Conf) test.Prepare(t, config.Conf)
defer test.Clean() defer test.Clean()
for _, widget := range preare(t) {
err := widget.LoadInstances()
if err != nil {
t.Fatal(err)
}
Load(config.Conf) instance, ok := widget.Instances.Load("feedback")
check(t) if !ok {
} t.Fatal("feedback instance not found")
}
func check(t *testing.T) { assert.Equal(t, "feedback", instance.(*Instance).id)
ids := map[string]bool{} assert.Equal(t, "feedback", instance.(*Instance).dsl.(map[string]interface{})["id"])
for id := range widget.Widgets {
ids[id] = true
} }
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
} }