Update dependencies in go.mod and go.sum to latest versions for improved stability and performance

This commit is contained in:
Max 2025-07-14 21:03:57 +08:00
parent 0b5d7fcc41
commit 06f5134083
12 changed files with 2101 additions and 0 deletions

47
dsl/api/api.go Normal file
View file

@ -0,0 +1,47 @@
package api
import (
"context"
"github.com/yaoapp/yao/dsl/types"
)
// YaoAPI is the MCP client DSL manager
type YaoAPI struct {
root string // The relative path of the MCP client DSL
}
// New returns a new connector DSL manager
func New(root string) types.Manager {
return &YaoAPI{root: root}
}
// Loaded return all loaded DSLs
func (api *YaoAPI) Loaded(ctx context.Context) (map[string]*types.Info, error) {
return nil, nil
}
// Load will unload the DSL first, then load the DSL from DB or file system
func (api *YaoAPI) Load(ctx context.Context, id string, options interface{}) error {
return nil
}
// Reload will unload the DSL first, then reload the DSL from DB or file system
func (api *YaoAPI) Reload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Unload will unload the DSL from memory
func (api *YaoAPI) Unload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Validate will validate the DSL from source
func (api *YaoAPI) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
return false, nil
}
// Execute will execute the DSL
func (api *YaoAPI) Execute(ctx context.Context, method string, args ...any) (any, error) {
return nil, nil
}

View file

@ -0,0 +1,47 @@
package connector
import (
"context"
"github.com/yaoapp/yao/dsl/types"
)
// YaoConnector is the connector DSL manager
type YaoConnector struct {
root string // The relative path of the connector DSL
}
// New returns a new connector DSL manager
func New(root string) types.Manager {
return &YaoConnector{root: root}
}
// Loaded return all loaded DSLs
func (c *YaoConnector) Loaded(ctx context.Context) (map[string]*types.Info, error) {
return nil, nil
}
// Load will unload the DSL first, then load the DSL from DB or file system
func (c *YaoConnector) Load(ctx context.Context, id string, options interface{}) error {
return nil
}
// Unload will unload the DSL from memory
func (c *YaoConnector) Unload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Reload will unload the DSL first, then reload the DSL from DB or file system
func (c *YaoConnector) Reload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Validate will validate the DSL from source
func (c *YaoConnector) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
return false, nil
}
// Execute will execute the DSL
func (c *YaoConnector) Execute(ctx context.Context, method string, args ...any) (any, error) {
return nil, nil
}

277
dsl/db.go Normal file
View file

@ -0,0 +1,277 @@
package dsl
import (
"fmt"
"strings"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/yao/dsl/types"
)
// getInfoFromDB get the info from the db
func (dsl *DSL) dbInspect(id string) (*types.Info, bool, error) {
// Get from database
m := model.Select("__yao.dsl")
// Get the info
var info types.Info
rows, err := m.Get(model.QueryParam{
Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}},
Select: []interface{}{
"dsl_id",
"type",
"label",
"path",
"sort",
"tags",
"description",
"status",
"store",
"mtime",
"ctime",
},
Limit: 1,
Orders: []model.QueryOrder{{Column: "sort", Option: "asc"}, {Column: "mtime", Option: "desc"}},
})
if err != nil {
return nil, false, err
}
if len(rows) == 0 {
return nil, false, nil
}
raw, err := jsoniter.Marshal(rows[0])
if err != nil {
return nil, false, err
}
err = jsoniter.Unmarshal(raw, &info)
if err != nil {
return nil, false, err
}
return &info, true, nil
}
// getSourceFromDB get the source from the db
func (dsl *DSL) dbSource(id string) (string, bool, error) {
// Get from database
m := model.Select("__yao.dsl")
// Get the source
rows, err := m.Get(model.QueryParam{
Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}},
Select: []interface{}{"source"},
Limit: 1,
})
if err != nil {
return "", false, err
}
if len(rows) == 0 {
return "", false, nil
}
source, ok := rows[0]["source"].(string)
if !ok {
return "", true, fmt.Errorf("%s %s source is not a string", dsl.Type, id)
}
return source, true, nil
}
// getListFromDB get the list from the db
func (dsl *DSL) dbList(options *types.ListOptions) ([]*types.Info, error) {
// Get from database
m := model.Select("__yao.dsl")
var orders []model.QueryOrder = []model.QueryOrder{{Column: "mtime", Option: "desc"}}
if options.Sort == "sort" {
orders = []model.QueryOrder{{Column: "sort", Option: "asc"}}
}
var wheres []model.QueryWhere = []model.QueryWhere{{Column: "type", Value: dsl.Type}}
// Filter by tags
if len(options.Tags) > 0 {
var orwheres []model.QueryWhere = []model.QueryWhere{}
for _, tag := range options.Tags {
match := "%" + strings.TrimSpace(tag) + "%"
orwheres = append(orwheres, model.QueryWhere{Column: "tags", Value: match, OP: "like", Method: "orwhere"})
}
wheres = append(wheres, model.QueryWhere{Wheres: orwheres})
}
// Get the list
rows, err := m.Get(model.QueryParam{
Wheres: []model.QueryWhere{{Column: "type", Value: dsl.Type}},
Select: []interface{}{"dsl_id", "label", "path", "sort", "tags", "description", "status", "store", "mtime", "ctime"},
Orders: orders,
})
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
var infos []*types.Info
raw, err := jsoniter.Marshal(rows)
if err != nil {
return nil, err
}
err = jsoniter.Unmarshal(raw, &infos)
if err != nil {
return nil, err
}
return infos, nil
}
func (dsl *DSL) dbCreate(options *types.CreateOptions) error {
if options.Source == "" {
return fmt.Errorf("%s %s source is required", dsl.Type, options.ID)
}
// Get info from source
var info types.Info
err := jsoniter.Unmarshal([]byte(options.Source), &info)
if err != nil {
return err
}
// Get the info
m := model.Select("__yao.dsl")
data := map[string]interface{}{
"source": options.Source,
"dsl_id": options.ID,
"type": dsl.Type,
"label": info.Label,
"path": info.Path,
"sort": info.Sort,
"tags": info.Tags,
"description": info.Description,
"status": info.Status,
"store": info.Store,
"mtime": time.Now().Unix(),
"ctime": time.Now().Unix(),
}
_, err = m.Create(data)
if err != nil {
return err
}
return nil
}
func (dsl *DSL) dbUpdate(options *types.UpdateOptions) error {
if options.Source == "" && options.Info == nil {
return fmt.Errorf("%s %s one of source or info is required", dsl.Type, options.ID)
}
m := model.Select("__yao.dsl")
// Check if the dsl exists
rows, err := m.Get(model.QueryParam{
Wheres: []model.QueryWhere{{Column: "dsl_id", Value: options.ID}},
Select: []interface{}{"id", "dsl_id"},
Limit: 1,
})
if err != nil {
return err
}
if len(rows) == 0 {
return fmt.Errorf("%s %s not found", dsl.Type, options.ID)
}
row := rows[0]
// update source
var data map[string]interface{} = map[string]interface{}{}
if options.Source != "" {
data["source"] = options.Source
} else {
// Update info
if options.Info.Label != "" {
data["label"] = options.Info.Label
}
if options.Info.Path != "" {
data["path"] = options.Info.Path
}
if options.Info.Sort != 0 {
data["sort"] = options.Info.Sort
}
if options.Info.Tags != nil {
data["tags"] = options.Info.Tags
}
if options.Info.Description != "" {
data["description"] = options.Info.Description
}
}
// Update the data
err = m.Update(row["id"], data)
if err != nil {
return err
}
return nil
}
func (dsl *DSL) dbDelete(id string) error {
// Get from database
m := model.Select("__yao.dsl")
// Check if the dsl exists
rows, err := m.Get(model.QueryParam{
Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}},
Select: []interface{}{"id", "dsl_id"},
Limit: 1,
})
if err != nil {
return err
}
if len(rows) == 0 {
return fmt.Errorf("%s %s not found", dsl.Type, id)
}
// Delete the dsl
row := rows[0]
return m.Delete(row["id"])
}
func (dsl *DSL) dbExists(id string) (bool, error) {
// Get from database
m := model.Select("__yao.dsl")
// Check if the dsl exists
rows, err := m.Get(model.QueryParam{
Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}},
Select: []interface{}{"id", "dsl_id"},
Limit: 1,
})
if err != nil {
return false, err
}
return len(rows) > 0, nil
}

319
dsl/dsl.go Normal file
View file

@ -0,0 +1,319 @@
package dsl
import (
"context"
"fmt"
"github.com/yaoapp/yao/dsl/api"
"github.com/yaoapp/yao/dsl/connector"
"github.com/yaoapp/yao/dsl/mcp"
"github.com/yaoapp/yao/dsl/model"
"github.com/yaoapp/yao/dsl/types"
)
// DSL is the base DSL struct
type DSL struct {
Type types.Type
exts []string
root string
manager types.Manager
}
// New returns a new DSL manager
func New(typ types.Type) (types.DSL, error) {
var manager types.Manager
// Get the root path and the extensions of the type
root, exts := types.TypeRootAndExts(typ)
// Create the manager
switch typ {
case types.TypeConnector:
exts = []string{".conn.yao", ".conn.jsonc", ".conn.json"}
manager = connector.New(root)
case types.TypeModel:
exts = []string{".mod.yao", ".mod.jsonc", ".mod.json"}
manager = model.New(root)
case types.TypeMCPClient:
exts = []string{".mcp.yao", ".mcp.jsonc", ".mcp.json"}
manager = mcp.NewClient(root)
// case types.TypeMCPServer:
// exts = []string{".mcp.yao", ".mcp.jsonc", ".mcp.json"}
// manager = mcp.NewServer(root)
case types.TypeAPI:
exts = []string{".http.yao", ".http.jsonc", ".http.json"}
manager = api.New(root)
default:
return nil, fmt.Errorf("dsl manager is not initialized, %s not supported", typ)
}
return &DSL{Type: typ, manager: manager, root: root, exts: exts}, nil
}
// Inspect DSL
func (dsl *DSL) Inspect(ctx context.Context, id string) (*types.Info, error) {
// Get the info from the db
info, exists, err := dsl.dbInspect(id)
if err != nil {
return nil, err
}
if !exists {
// Get the info from the file
info, exists, err = dsl.fsInspect(types.ToPath(dsl.Type, id))
if err != nil {
return nil, err
}
if !exists {
return nil, fmt.Errorf("dsl not found, %s", id)
}
}
// Merge the status from the manager
loaded, err := dsl.manager.Loaded(ctx)
if err != nil {
return info, err
}
// Check if the DSL is loaded
if _, ok := loaded[id]; ok {
info.Status = types.StatusLoaded
}
return info, nil
}
// Path Get Path by id, ( If the DSL is saved as file, return the file path )
func (dsl *DSL) Path(ctx context.Context, id string) (string, error) {
return types.ToPath(dsl.Type, id), nil
}
// Source Get Source by id
func (dsl *DSL) Source(ctx context.Context, id string) (string, error) {
// Get the source from the db
source, exists, err := dsl.dbSource(id)
if err != nil {
return "", err
}
if !exists {
// Get the source from the file
source, exists, err = dsl.fsSource(types.ToPath(dsl.Type, id))
if err != nil {
return "", err
}
if !exists {
return "", fmt.Errorf("%s DSL not found, %s", dsl.Type, id)
}
}
return source, nil
}
// List DSLs
func (dsl *DSL) List(ctx context.Context, opts *types.ListOptions) ([]*types.Info, error) {
// Get the list from the db
dbList, err := dsl.dbList(opts)
if err != nil {
return nil, err
}
// Get the list from the file
fileList, err := dsl.fsList(opts)
if err != nil {
return nil, err
}
// Merge the list and unique
list := []*types.Info{}
unique := make(map[string]bool)
for _, info := range dbList {
if _, ok := unique[info.ID]; !ok {
list = append(list, info)
unique[info.ID] = true
}
}
for _, info := range fileList {
if _, ok := unique[info.ID]; !ok {
list = append(list, info)
unique[info.ID] = true
}
}
// Merge the status from the manager
loaded, err := dsl.manager.Loaded(ctx)
if err != nil {
return nil, err
}
// Merge the status from the manager
for _, info := range list {
if _, ok := loaded[info.ID]; ok {
info.Status = types.StatusLoaded
}
}
return list, nil
}
// Create DSL
func (dsl *DSL) Create(ctx context.Context, options *types.CreateOptions) error {
if options.Store == types.StoreTypeDB {
err := dsl.dbCreate(options)
if err != nil {
return err
}
}
if options.Store == types.StoreTypeFile {
err := dsl.fsCreate(options)
if err != nil {
return err
}
}
// Load the DSL
err := dsl.Load(ctx, options.ID, options.LoadOptions)
if err != nil {
return err
}
return nil
}
// Exists Check if the DSL exists
func (dsl *DSL) Exists(ctx context.Context, id string) (bool, error) {
// Check if the DSL exists in the db
exists, err := dsl.dbExists(id)
if err != nil {
return false, err
}
if exists {
return true, nil
}
// Check if the DSL exists in the file
return dsl.fsExists(id)
}
// Update DSL
func (dsl *DSL) Update(ctx context.Context, options *types.UpdateOptions) error {
// Exists
info, exists, err := dsl.dbInspect(options.ID)
if err != nil {
return err
}
if !exists {
info, exists, err = dsl.fsInspect(types.ToPath(dsl.Type, options.ID))
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s DSL not found, %s", dsl.Type, options.ID)
}
}
// Update the DSL in the db
if info.Store == types.StoreTypeDB {
err := dsl.dbUpdate(options)
if err != nil {
return err
}
// Reload the DSL
return dsl.manager.Reload(ctx, options.ID, options.ReloadOptions)
}
// Update the DSL in the file
err = dsl.fsUpdate(options)
if err != nil {
return err
}
// Reload the DSL
return dsl.manager.Reload(ctx, options.ID, options.ReloadOptions)
}
// Delete DSL
func (dsl *DSL) Delete(ctx context.Context, id string, options ...interface{}) error {
// Exists
info, exists, err := dsl.dbInspect(id)
if err != nil {
return err
}
if !exists {
info, exists, err = dsl.fsInspect(types.ToPath(dsl.Type, id))
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s DSL not found, %s", dsl.Type, id)
}
}
var unloadOptions interface{}
if len(options) > 0 {
unloadOptions = options[0]
}
if info.Store == types.StoreTypeDB {
err = dsl.dbDelete(id)
if err != nil {
return err
}
// Unload the DSL
return dsl.manager.Unload(ctx, id, unloadOptions)
}
err = dsl.fsDelete(id)
if err != nil {
return err
}
// Unload the DSL
return dsl.manager.Unload(ctx, id, unloadOptions)
}
// Load DSL
func (dsl *DSL) Load(ctx context.Context, id string, options interface{}) error {
return dsl.manager.Load(ctx, id, options)
}
// Unload DSL
func (dsl *DSL) Unload(ctx context.Context, id string, options ...interface{}) error {
var unloadOptions interface{}
if len(options) > 0 {
unloadOptions = options[0]
}
return dsl.manager.Unload(ctx, id, unloadOptions)
}
// Reload DSL
func (dsl *DSL) Reload(ctx context.Context, id string, options interface{}) error {
return dsl.manager.Reload(ctx, id, options)
}
// Execute DSL (Some DSLs can be executed)
func (dsl *DSL) Execute(ctx context.Context, method string, args ...any) (any, error) {
return dsl.manager.Execute(ctx, method, args...)
}
// Validate DSL
func (dsl *DSL) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
return dsl.manager.Validate(ctx, source)
}

203
dsl/fs.go Normal file
View file

@ -0,0 +1,203 @@
package dsl
import (
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/yao/dsl/types"
)
// getInfoFromFile get the info from the file
func (dsl *DSL) fsInspect(id string, path ...string) (*types.Info, bool, error) {
file := ""
if len(path) > 0 {
file = path[0]
} else {
file = types.ToPath(dsl.Type, id)
}
var info types.Info = types.Info{ID: id, Path: file}
exists, err := application.App.Exists(file)
if err != nil {
return nil, false, err
}
if !exists {
return nil, false, nil
}
// Read the file
data, err := application.App.Read(file)
if err != nil {
return nil, false, err
}
// Unmarshal the data to the info
err = application.Parse(file, data, &info)
if err != nil {
return nil, true, err
}
// Merge the mtime and ctime
fileInfo, err := application.App.Info(file)
if err != nil {
return nil, true, err
}
info.Mtime = fileInfo.ModTime()
info.Ctime = fileInfo.ModTime()
return &info, true, nil
}
// getSourceFromFile get the source from the file
func (dsl *DSL) fsSource(id string) (string, bool, error) {
path := types.ToPath(dsl.Type, id)
exists, err := application.App.Exists(path)
if err != nil {
return "", false, err
}
if !exists {
return "", false, nil
}
// Read the file
data, err := application.App.Read(path)
if err != nil {
return "", false, err
}
return string(data), true, nil
}
// getListFromPath get the list from the path
func (dsl *DSL) fsList(options *types.ListOptions) ([]*types.Info, error) {
root, exts := types.TypeRootAndExts(dsl.Type)
var infos []*types.Info = []*types.Info{}
patterns := []string{}
for _, ext := range exts {
patterns = append(patterns, "*"+ext)
}
var errs []error
err := application.App.Walk(root, func(root, file string, isdir bool) error {
if isdir {
return nil
}
id := types.WithTypeToID(dsl.Type, file)
info, _, err := dsl.fsInspect(id, file)
if err != nil {
errs = append(errs, err)
return nil
}
// Filter by options
if len(options.Tags) > 0 {
if len(info.Tags) == 0 {
return nil
}
for _, tag := range options.Tags {
for _, t := range info.Tags {
if t == tag {
infos = append(infos, info)
return nil
}
}
}
}
// Add to the list
infos = append(infos, info)
return err
}, patterns...)
return infos, err
}
func (dsl *DSL) fsCreate(options *types.CreateOptions) error {
path := types.ToPath(dsl.Type, options.ID)
// Check if the file is a directory
exists, err := application.App.Exists(path)
if err != nil {
return err
}
if exists {
return fmt.Errorf("%v %s already exists", dsl.Type, options.ID)
}
// Create the file
return application.App.Write(path, []byte(options.Source))
}
func (dsl *DSL) fsUpdate(options *types.UpdateOptions) error {
// Validate the options
if options.Source == "" && options.Info == nil {
return fmt.Errorf("%v %s one of source or info is required", dsl.Type, options.ID)
}
path := types.ToPath(dsl.Type, options.ID)
// Check if the file exists
exists, err := application.App.Exists(path)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%v %s not found", dsl.Type, options.ID)
}
// Update source
if options.Source != "" {
return application.App.Write(path, []byte(options.Source))
}
// Update info
var source map[string]interface{}
data, err := application.App.Read(path)
if err != nil {
return err
}
err = application.Parse(path, data, &source)
if err != nil {
return err
}
// Update the info
source["id"] = options.ID
source["label"] = options.Info.Label
source["tags"] = options.Info.Tags
source["description"] = options.Info.Description
new, err := jsoniter.MarshalIndent(source, "", " ")
if err != nil {
return err
}
return application.App.Write(path, []byte(new))
}
func (dsl *DSL) fsDelete(id string) error {
path := types.ToPath(dsl.Type, id)
// Check if the file is a directory
exists, err := application.App.Exists(path)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%v %s not found", dsl.Type, id)
}
// Delete the file
return application.App.Remove(path)
}
func (dsl *DSL) fsExists(id string) (bool, error) {
path := types.ToPath(dsl.Type, id)
return application.App.Exists(path)
}

52
dsl/mcp/client.go Normal file
View file

@ -0,0 +1,52 @@
package mcp
import (
"context"
"github.com/yaoapp/yao/dsl/types"
)
// YaoMCPClient is the MCP client DSL manager
type YaoMCPClient struct {
root string // The relative path of the MCP client DSL
}
// NewClient returns a new MCP client DSL manager
func NewClient(root string) types.Manager {
return New(root)
}
// New returns a new connector DSL manager
func New(root string) types.Manager {
return &YaoMCPClient{root: root}
}
// Loaded return all loaded DSLs
func (client *YaoMCPClient) Loaded(ctx context.Context) (map[string]*types.Info, error) {
return nil, nil
}
// Load will unload the DSL first, then load the DSL from DB or file system
func (client *YaoMCPClient) Load(ctx context.Context, id string, options interface{}) error {
return nil
}
// Unload will unload the DSL from memory
func (client *YaoMCPClient) Unload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Reload will unload the DSL first, then reload the DSL from DB or file system
func (client *YaoMCPClient) Reload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Validate will validate the DSL from source
func (client *YaoMCPClient) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
return false, nil
}
// Execute will execute the DSL
func (client *YaoMCPClient) Execute(ctx context.Context, method string, args ...any) (any, error) {
return nil, nil
}

47
dsl/mcp/server.go Normal file
View file

@ -0,0 +1,47 @@
package mcp
import (
"context"
"github.com/yaoapp/yao/dsl/types"
)
// YaoMCPServer is the MCP client DSL manager
type YaoMCPServer struct {
root string // The relative path of the MCP client DSL
}
// NewServer returns a new MCP server DSL manager
func NewServer(root string) types.Manager {
return &YaoMCPServer{root: root}
}
// Loaded return all loaded DSLs
func (server *YaoMCPServer) Loaded(ctx context.Context) (map[string]*types.Info, error) {
return nil, nil
}
// Load will unload the DSL first, then load the DSL from DB or file system
func (server *YaoMCPServer) Load(ctx context.Context, id string, options interface{}) error {
return nil
}
// Unload will unload the DSL from memory
func (server *YaoMCPServer) Unload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Reload will unload the DSL first, then reload the DSL from DB or file system
func (server *YaoMCPServer) Reload(ctx context.Context, id string, options interface{}) error {
return nil
}
// Validate will validate the DSL from source
func (server *YaoMCPServer) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
return false, nil
}
// Execute will execute the DSL
func (server *YaoMCPServer) Execute(ctx context.Context, method string, args ...any) (any, error) {
return nil, nil
}

139
dsl/model/model.go Normal file
View file

@ -0,0 +1,139 @@
package model
import (
"context"
"fmt"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/yao/dsl/types"
)
// YaoModel is the MCP client DSL manager
type YaoModel struct {
root string // The relative path of the MCP client DSL
}
// NewClient returns a new MCP client DSL manager
func NewClient(root string) types.Manager {
return New(root)
}
// New returns a new connector DSL manager
func New(root string) types.Manager {
return &YaoModel{root: root}
}
// Loaded return all loaded DSLs
func (m *YaoModel) Loaded(ctx context.Context) (map[string]*types.Info, error) {
infos := map[string]*types.Info{}
for id, mod := range model.Models {
infos[id] = &types.Info{
ID: id,
Type: types.TypeModel,
Label: mod.MetaData.Name,
Path: mod.File,
Sort: 999,
Tags: []string{},
Description: "Description",
}
}
return infos, nil
}
// Load will unload the DSL first, then load the DSL from DB or file system
func (m *YaoModel) Load(ctx context.Context, id string, options interface{}) error {
var opts map[string]interface{}
if v, ok := options.(map[string]interface{}); ok {
opts = v
}
var migration bool = false
if v, ok := opts["migration"]; ok {
migration = v.(bool)
}
var reset bool = false
if v, ok := opts["reset"]; ok {
reset = v.(bool)
}
path := types.ToPath(types.TypeModel, id)
mod, err := model.LoadSync(path, id)
if err != nil {
return err
}
if migration || reset {
return mod.Migrate(reset, model.WithDonotInsertValues(true))
}
return nil
}
// Unload will unload the DSL from memory
func (m *YaoModel) Unload(ctx context.Context, id string, options interface{}) error {
var opts map[string]interface{}
if v, ok := options.(map[string]interface{}); ok {
opts = v
}
var dropTable bool = false
if v, ok := opts["dropTable"]; ok {
dropTable = v.(bool)
}
mod := model.Select(id)
if mod == nil {
return fmt.Errorf("model %s not found", id)
}
if dropTable {
return mod.DropTable()
}
return nil
}
// Reload will unload the DSL first, then reload the DSL from DB or file system
func (m *YaoModel) Reload(ctx context.Context, id string, options interface{}) error {
var opts map[string]interface{}
if v, ok := options.(map[string]interface{}); ok {
opts = v
}
var migration bool = false
if v, ok := opts["migration"]; ok {
migration = v.(bool)
}
var reset bool = false
if v, ok := opts["reset"]; ok {
reset = v.(bool)
}
// Reload the model
path := types.ToPath(types.TypeModel, id)
mod, err := model.LoadSync(path, id)
if err != nil {
return err
}
if migration || reset {
return mod.Migrate(reset, model.WithDonotInsertValues(true))
}
return nil
}
// Validate will validate the DSL from source
func (m *YaoModel) Validate(ctx context.Context, source string) (bool, []types.LintMessage) {
return true, []types.LintMessage{}
}
// Execute will execute the DSL
func (m *YaoModel) Execute(ctx context.Context, method string, args ...any) (any, error) {
return nil, fmt.Errorf("Not implemented")
}

49
dsl/types/interfaces.go Normal file
View file

@ -0,0 +1,49 @@
package types
import "context"
// DSL interface
type DSL interface {
Inspect(ctx context.Context, id string) (*Info, error) // Inspect DSL
Path(ctx context.Context, id string) (string, error) // Get Path by id, ( If the DSL is saved as file, return the file path )
Source(ctx context.Context, id string) (string, error) // Get Source by id
List(ctx context.Context, opts *ListOptions) ([]*Info, error) // List All DSLs including unloaded/error DSLs
Exists(ctx context.Context, id string) (bool, error) // Check if the DSL exists
// DSL Operations
Create(ctx context.Context, options *CreateOptions) error // Create DSL, Create will unload the DSL first, then create the DSL to DB
Update(ctx context.Context, options *UpdateOptions) error // Update DSL, Update will unload the DSL first, then update the DSL, if update info only, will not unload the DSL
Delete(ctx context.Context, id string, unloadOptions ...interface{}) error // Delete DSL, Delete will unload the DSL first, then delete the DSL file
// Load manager
Load(ctx context.Context, id string, options interface{}) error // Load DSL, Load will unload the DSL first, then load the DSL from DB or file system
Reload(ctx context.Context, id string, options interface{}) error // Reload DSL, Reload will unload the DSL first, then reload the DSL from DB or file system
Unload(ctx context.Context, id string, options ...interface{}) error // Unload DSL, Unload will unload the DSL from memory
// Execute
Execute(ctx context.Context, method string, args ...any) (any, error) // Execute DSL (Some DSLs can be executed)
// Validate
Validate(ctx context.Context, source string) (bool, []LintMessage) // Validate DSL, Validate will validate the DSL from source
}
// Manager interface
type Manager interface {
// Get all loaded DSLs
Loaded(ctx context.Context) (map[string]*Info, error) // Get all loaded DSLs
// Load DSL, Load will unload the DSL first, then load the DSL from DB or file system
Load(ctx context.Context, id string, options interface{}) error
// Unload DSL, Unload will unload the DSL from memory
Unload(ctx context.Context, id string, options interface{}) error
// Reload DSL, Reload will unload the DSL first, then reload the DSL from DB or file system
Reload(ctx context.Context, id string, options interface{}) error
// Validate DSL, Validate will validate the DSL from source
Validate(ctx context.Context, source string) (bool, []LintMessage)
// Execute DSL (Some DSLs can be executed)
Execute(ctx context.Context, method string, args ...any) (any, error)
}

126
dsl/types/types.go Normal file
View file

@ -0,0 +1,126 @@
package types
import (
"time"
)
// Type for DSL
type Type string
// Status for DSL
type Status string
// StoreType for DSL store
type StoreType string
// LintSeverity for DSL linter
type LintSeverity string
// StoreType for DSL store
const (
StoreTypeDB StoreType = "db"
StoreTypeFile StoreType = "file"
)
// Status for DSL
const (
StatusLoading Status = "loading"
StatusLoaded Status = "loaded"
StatusError Status = "error"
)
// LintSeverity for DSL linter
const (
LintSeverityError LintSeverity = "error"
LintSeverityWarning LintSeverity = "warning"
LintSeverityInfo LintSeverity = "info"
LintSeverityHint LintSeverity = "hint"
)
// Type for DSL
const (
// TypeModel for model
TypeModel Type = "model"
// TypeAPI for api
TypeAPI Type = "api"
// TypeConnector for connector
TypeConnector Type = "connector"
// TypeMCPServer for MCP server
TypeMCPServer Type = "mcp-server"
// TypeMCPClient for MCP client
TypeMCPClient Type = "mcp-client"
// TypeStore for store
TypeStore Type = "store"
// TypeSchedule for schedule
TypeSchedule Type = "schedule"
// TypeTable for table
TypeTable Type = "table"
// TypeForm for form
TypeForm Type = "form"
// TypeList for list
TypeList Type = "list"
// TypeChart for chart
TypeChart Type = "chart"
// TypeDashboard for dashboard
TypeDashboard Type = "dashboard"
// TypeFlow for flow
TypeFlow Type = "flow"
// TypePipe for pipe
TypePipe Type = "pipe"
// TypeAIGC for aigc
TypeAIGC Type = "aigc"
// TypeUnknown for unknown
TypeUnknown Type = "unknown"
)
// Info for DSL
type Info struct {
ID string
Type Type
Sort int
Path string
Label string
Description string
Tags []string
Status Status
Store StoreType
Mtime time.Time
Ctime time.Time
}
// ListOptions for DSL list
type ListOptions struct {
Sort string
Order string
Tags []string
}
// CreateOptions for DSL upsert
type CreateOptions struct {
ID string // ID is the id of the DSL, if not provided, a new id will be generated, required
Source string // Source is the source of the DSL, if not provided, the DSL will be loaded from the file system
Store StoreType // Store is the store type of the DSL, if not provided, the DSL will be loaded from the file system
LoadOptions interface{} // LoadOptions is the options for the DSL, if not provided, the DSL will be loaded from the file system
}
// UpdateOptions for DSL upsert
type UpdateOptions struct {
ID string // ID is the id of the DSL, if not provided, a new id will be generated, required
Info *Info // Info is the info of the DSL, if not provided, the DSL will be loaded from the file system, one of info or source must be provided
Source string // Source is the source of the DSL, if not provided, the DSL will be loaded from the file system, one of info or source must be provided
ReloadOptions interface{} // ReloadOptions is the options for the DSL, if not provided, the DSL will be loaded from the file system
}
// LintMessage for DSL linter
type LintMessage struct {
File string
Line int
Column int
Message string
Severity LintSeverity
}
var lintMessages []LintMessage

194
dsl/types/utils.go Normal file
View file

@ -0,0 +1,194 @@
package types
import (
"os"
"path/filepath"
"strings"
)
// ToPath convert id to path
func ToPath(typ Type, id string) string {
// Get the root path and the extensions of the type
root, exts := TypeRootAndExts(typ)
ext := ".yao"
if len(exts) > 0 {
ext = exts[0]
}
// 1. Replace all . to /
path := strings.ReplaceAll(id, ".", string(os.PathSeparator))
// 2. Replace all __ to .
path = strings.ReplaceAll(path, "__", ".")
// 3. Join the root path
return filepath.Join(root, path) + ext
}
// ToID convert file path to id
func ToID(path string) string {
typ := DetectType(path)
return WithTypeToID(typ, path)
}
// WithTypeToID convert file path to id
func WithTypeToID(typ Type, path string) string {
// Get the root path and the extensions of the type
root, exts := TypeRootAndExts(typ)
// 0. if the first character is /, remove it
if strings.HasPrefix(path, string(os.PathSeparator)) {
path = strings.TrimPrefix(path, string(os.PathSeparator))
}
// 1. Split the path by /
parts := strings.Split(path, string(os.PathSeparator))
if len(parts) > 0 && parts[0] == root {
// Skip the root path
parts = parts[1:]
// Remove the extension only if parts is not empty
if len(parts) > 0 {
last := parts[len(parts)-1]
for _, ext := range exts {
if strings.HasSuffix(last, ext) {
parts[len(parts)-1] = strings.TrimSuffix(last, ext)
}
}
}
// Join the parts
path = strings.Join(parts, string(os.PathSeparator))
}
// 2. Replace All . to __
path = strings.ReplaceAll(path, ".", "__")
// 3. Replace all / to .
path = strings.ReplaceAll(path, string(os.PathSeparator), ".")
return path
}
// DetectType detect the type by the file path
func DetectType(path string) Type {
parts := strings.Split(path, string(os.PathSeparator))
if len(parts) < 2 {
return TypeUnknown
}
root := parts[0]
last := parts[len(parts)-1]
extParts := strings.Split(last, ".")
if len(extParts) < 2 {
return TypeUnknown
}
ext := extParts[len(extParts)-2]
// Detect the type by the extension
switch ext {
case "http":
return TypeAPI
case "sch":
return TypeSchedule
case "table":
return TypeTable
case "form":
return TypeForm
case "list":
return TypeList
case "chart":
return TypeChart
case "dash":
return TypeDashboard
case "flow":
return TypeFlow
case "pipe":
return TypePipe
case "ai":
return TypeAIGC
case "mod":
return TypeModel
case "conn":
return TypeConnector
case "lru", "redis", "mongo", "badger":
return TypeStore
}
// Detect the type by the root path
switch root {
case "models":
return TypeModel
case "connectors":
return TypeConnector
case "mcps":
return TypeMCPClient
case "apis":
if ext == "http" {
return TypeAPI
}
if ext == "mcp" {
return TypeMCPServer
}
return TypeUnknown
case "schedules":
return TypeSchedule
case "tables":
return TypeTable
case "forms":
return TypeForm
case "lists":
return TypeList
case "charts":
return TypeChart
case "dashboards":
return TypeDashboard
case "flows":
return TypeFlow
case "pipes":
return TypePipe
case "aigcs":
return TypeAIGC
case "stores":
return TypeStore
default:
return TypeUnknown
}
}
// TypeRootAndExts return the root path and the extensions of the type
func TypeRootAndExts(typ Type) (string, []string) {
switch typ {
case TypeModel:
return "models", []string{".mod.yao", ".mod.jsonc", ".mod.json"}
case TypeConnector:
return "connectors", []string{".conn.yao", ".conn.jsonc", ".conn.json"}
case TypeMCPClient, TypeMCPServer:
return "mcps", []string{".mcp.yao", ".mcp.jsonc", ".mcp.json"}
case TypeAPI:
return "apis", []string{".http.yao", ".http.jsonc", ".http.json"}
case TypeSchedule:
return "schedules", []string{".sch.yao", ".sch.jsonc", ".sch.json"}
case TypeTable:
return "tables", []string{".table.yao", ".table.jsonc", ".table.json"}
case TypeForm:
return "forms", []string{".form.yao", ".form.jsonc", ".form.json"}
case TypeList:
return "lists", []string{".list.yao", ".list.jsonc", ".list.json"}
case TypeChart:
return "charts", []string{".chart.yao", ".chart.jsonc", ".chart.json"}
case TypeDashboard:
return "dashboards", []string{".dash.yao", ".dash.jsonc", ".dash.json"}
case TypeFlow:
return "flows", []string{".flow.yao", ".flow.jsonc", ".flow.json"}
case TypePipe:
return "pipes", []string{".pipe.yao", ".pipe.jsonc", ".pipe.json"}
case TypeAIGC:
return "aigcs", []string{".ai.yao", ".ai.jsonc", ".ai.json"}
case TypeStore:
return "stores", []string{".lru.yao", ".redis.yao", ".mongo.yao", ".badger.yao", ".store.yao", ".store.jsonc", ".store.json"}
default:
return "", []string{}
}
}

601
dsl/types/utils_test.go Normal file
View file

@ -0,0 +1,601 @@
package types
import (
"os"
"path/filepath"
"testing"
)
func TestToPath(t *testing.T) {
tests := []struct {
name string
typ Type
id string
want string
}{
{
name: "Model with dots and underscores",
typ: TypeModel,
id: "user__profile.admin",
want: filepath.Join("models", "user.profile", "admin.mod.yao"),
},
{
name: "API with simple id",
typ: TypeAPI,
id: "user.login",
want: filepath.Join("apis", "user", "login.http.yao"),
},
{
name: "Unknown type (defaults to .yao)",
typ: TypeUnknown,
id: "test",
want: filepath.Join("", "test.yao"),
},
{
name: "Connector with nested path",
typ: TypeConnector,
id: "database.mysql__config",
want: filepath.Join("connectors", "database", "mysql.config.conn.yao"),
},
{
name: "Type with no extensions",
typ: Type("unknown"),
id: "test",
want: filepath.Join("", "test.yao"),
},
{
name: "Type with empty extensions",
typ: Type(""),
id: "test",
want: filepath.Join("", "test.yao"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ToPath(tt.typ, tt.id); got != tt.want {
t.Errorf("ToPath() = %v, want %v", got, tt.want)
}
})
}
}
func TestToID(t *testing.T) {
tests := []struct {
name string
path string
want string
}{
{
name: "Model file path",
path: filepath.Join("models", "user.mod.yao"),
want: "user",
},
{
name: "API file path",
path: filepath.Join("apis", "user", "login.http.yao"),
want: "user.login",
},
{
name: "Form file path with dots",
path: filepath.Join("forms", "user.profile", "edit.form.yao"),
want: "user__profile.edit",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ToID(tt.path); got != tt.want {
t.Errorf("ToID() = %v, want %v", got, tt.want)
}
})
}
}
func TestWithTypeToID(t *testing.T) {
tests := []struct {
name string
typ Type
path string
want string
}{
{
name: "Path with leading separator",
typ: TypeModel,
path: string(os.PathSeparator) + filepath.Join("models", "user.mod.yao"),
want: "user",
},
{
name: "Path without leading separator",
typ: TypeModel,
path: filepath.Join("models", "user.mod.yao"),
want: "user",
},
{
name: "Path with root not matching",
typ: TypeModel,
path: filepath.Join("other", "user.mod.yao"),
want: "other.user__mod__yao",
},
{
name: "Nested path with dots",
typ: TypeForm,
path: filepath.Join("forms", "user.profile", "edit.form.yao"),
want: "user__profile.edit",
},
{
name: "Multiple extensions matching",
typ: TypeModel,
path: filepath.Join("models", "user.mod.jsonc"),
want: "user",
},
{
name: "No extension matching",
typ: TypeModel,
path: filepath.Join("models", "user.txt"),
want: "user__txt",
},
{
name: "Path with single part",
typ: TypeModel,
path: "user.mod.yao",
want: "user__mod__yao",
},
{
name: "Store type with multiple extensions",
typ: TypeStore,
path: filepath.Join("stores", "cache.redis.yao"),
want: "cache",
},
{
name: "Empty path",
typ: TypeModel,
path: "",
want: "",
},
{
name: "Path with root matching but no parts",
typ: TypeModel,
path: "models",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := WithTypeToID(tt.typ, tt.path); got != tt.want {
t.Errorf("WithTypeToID() = %v, want %v", got, tt.want)
}
})
}
}
func TestDetectType(t *testing.T) {
tests := []struct {
name string
path string
want Type
}{
// Test by extension
{
name: "HTTP API",
path: filepath.Join("apis", "user.http.yao"),
want: TypeAPI,
},
{
name: "Schedule",
path: filepath.Join("schedules", "backup.sch.yao"),
want: TypeSchedule,
},
{
name: "Table",
path: filepath.Join("tables", "user.table.yao"),
want: TypeTable,
},
{
name: "Form",
path: filepath.Join("forms", "user.form.yao"),
want: TypeForm,
},
{
name: "List",
path: filepath.Join("lists", "user.list.yao"),
want: TypeList,
},
{
name: "Chart",
path: filepath.Join("charts", "sales.chart.yao"),
want: TypeChart,
},
{
name: "Dashboard",
path: filepath.Join("dashboards", "main.dash.yao"),
want: TypeDashboard,
},
{
name: "Flow",
path: filepath.Join("flows", "process.flow.yao"),
want: TypeFlow,
},
{
name: "Pipe",
path: filepath.Join("pipes", "transform.pipe.yao"),
want: TypePipe,
},
{
name: "AIGC",
path: filepath.Join("aigcs", "chat.ai.yao"),
want: TypeAIGC,
},
{
name: "Model by extension",
path: filepath.Join("models", "user.mod.yao"),
want: TypeModel,
},
{
name: "Connector by extension",
path: filepath.Join("connectors", "db.conn.yao"),
want: TypeConnector,
},
{
name: "Store LRU",
path: filepath.Join("stores", "cache.lru.yao"),
want: TypeStore,
},
{
name: "LRU extension in non-stores directory",
path: filepath.Join("other", "cache.lru.yao"),
want: TypeStore,
},
{
name: "Store Redis",
path: filepath.Join("stores", "cache.redis.yao"),
want: TypeStore,
},
{
name: "Store Mongo",
path: filepath.Join("stores", "cache.mongo.yao"),
want: TypeStore,
},
{
name: "Store Badger",
path: filepath.Join("stores", "cache.badger.yao"),
want: TypeStore,
},
{
name: "Store by extension",
path: filepath.Join("stores", "cache.store.yao"),
want: TypeStore,
},
{
name: "MCP extension in non-apis directory",
path: filepath.Join("other", "service.mcp.yao"),
want: TypeUnknown,
},
// Test by root path
{
name: "Model by root",
path: filepath.Join("models", "user.yao"),
want: TypeModel,
},
{
name: "Connector by root",
path: filepath.Join("connectors", "db.yao"),
want: TypeConnector,
},
{
name: "MCP Client",
path: filepath.Join("mcps", "client.yao"),
want: TypeMCPClient,
},
{
name: "API by root with http ext",
path: filepath.Join("apis", "user.http.yao"),
want: TypeAPI,
},
{
name: "MCP Server",
path: filepath.Join("apis", "server.mcp.yao"),
want: TypeMCPServer,
},
{
name: "MCP by extension",
path: filepath.Join("mcps", "client.mcp.yao"),
want: TypeMCPClient,
},
{
name: "API by root unknown ext",
path: filepath.Join("apis", "user.unknown.yao"),
want: TypeUnknown,
},
{
name: "Schedule by root",
path: filepath.Join("schedules", "backup.yao"),
want: TypeSchedule,
},
{
name: "Table by root",
path: filepath.Join("tables", "user.yao"),
want: TypeTable,
},
{
name: "Form by root",
path: filepath.Join("forms", "user.yao"),
want: TypeForm,
},
{
name: "List by root",
path: filepath.Join("lists", "user.yao"),
want: TypeList,
},
{
name: "Chart by root",
path: filepath.Join("charts", "sales.yao"),
want: TypeChart,
},
{
name: "Dashboard by root",
path: filepath.Join("dashboards", "main.yao"),
want: TypeDashboard,
},
{
name: "Flow by root",
path: filepath.Join("flows", "process.yao"),
want: TypeFlow,
},
{
name: "Pipe by root",
path: filepath.Join("pipes", "transform.yao"),
want: TypePipe,
},
{
name: "AIGC by root",
path: filepath.Join("aigcs", "chat.yao"),
want: TypeAIGC,
},
{
name: "Store by root",
path: filepath.Join("stores", "cache.yao"),
want: TypeStore,
},
{
name: "Unknown root",
path: filepath.Join("unknown", "file.yao"),
want: TypeUnknown,
},
// Edge cases
{
name: "Path with less than 2 parts",
path: "file.yao",
want: TypeUnknown,
},
{
name: "File without extension",
path: filepath.Join("models", "user"),
want: TypeUnknown,
},
{
name: "File with single dot",
path: filepath.Join("models", "user.yao"),
want: TypeModel,
},
{
name: "Empty path",
path: "",
want: TypeUnknown,
},
{
name: "Path with single component",
path: "file",
want: TypeUnknown,
},
{
name: "File with extension parts length < 2",
path: filepath.Join("models", "user"),
want: TypeUnknown,
},
{
name: "File with extension matching filename",
path: filepath.Join("models", "http.yao"),
want: TypeAPI,
},
{
name: "File with extension matching filename - sch",
path: filepath.Join("schedules", "sch.yao"),
want: TypeSchedule,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := DetectType(tt.path); got != tt.want {
t.Errorf("DetectType() = %v, want %v", got, tt.want)
}
})
}
}
func TestTypeRootAndExts(t *testing.T) {
tests := []struct {
name string
typ Type
wantRoot string
wantExts []string
}{
{
name: "Model",
typ: TypeModel,
wantRoot: "models",
wantExts: []string{".mod.yao", ".mod.jsonc", ".mod.json"},
},
{
name: "Connector",
typ: TypeConnector,
wantRoot: "connectors",
wantExts: []string{".conn.yao", ".conn.jsonc", ".conn.json"},
},
{
name: "MCP Client",
typ: TypeMCPClient,
wantRoot: "mcps",
wantExts: []string{".mcp.yao", ".mcp.jsonc", ".mcp.json"},
},
{
name: "MCP Server",
typ: TypeMCPServer,
wantRoot: "mcps",
wantExts: []string{".mcp.yao", ".mcp.jsonc", ".mcp.json"},
},
{
name: "API",
typ: TypeAPI,
wantRoot: "apis",
wantExts: []string{".http.yao", ".http.jsonc", ".http.json"},
},
{
name: "Schedule",
typ: TypeSchedule,
wantRoot: "schedules",
wantExts: []string{".sch.yao", ".sch.jsonc", ".sch.json"},
},
{
name: "Table",
typ: TypeTable,
wantRoot: "tables",
wantExts: []string{".table.yao", ".table.jsonc", ".table.json"},
},
{
name: "Form",
typ: TypeForm,
wantRoot: "forms",
wantExts: []string{".form.yao", ".form.jsonc", ".form.json"},
},
{
name: "List",
typ: TypeList,
wantRoot: "lists",
wantExts: []string{".list.yao", ".list.jsonc", ".list.json"},
},
{
name: "Chart",
typ: TypeChart,
wantRoot: "charts",
wantExts: []string{".chart.yao", ".chart.jsonc", ".chart.json"},
},
{
name: "Dashboard",
typ: TypeDashboard,
wantRoot: "dashboards",
wantExts: []string{".dash.yao", ".dash.jsonc", ".dash.json"},
},
{
name: "Flow",
typ: TypeFlow,
wantRoot: "flows",
wantExts: []string{".flow.yao", ".flow.jsonc", ".flow.json"},
},
{
name: "Pipe",
typ: TypePipe,
wantRoot: "pipes",
wantExts: []string{".pipe.yao", ".pipe.jsonc", ".pipe.json"},
},
{
name: "AIGC",
typ: TypeAIGC,
wantRoot: "aigcs",
wantExts: []string{".ai.yao", ".ai.jsonc", ".ai.json"},
},
{
name: "Store",
typ: TypeStore,
wantRoot: "stores",
wantExts: []string{".lru.yao", ".redis.yao", ".mongo.yao", ".badger.yao", ".store.yao", ".store.jsonc", ".store.json"},
},
{
name: "Unknown",
typ: TypeUnknown,
wantRoot: "",
wantExts: []string{},
},
{
name: "Empty type",
typ: Type(""),
wantRoot: "",
wantExts: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotRoot, gotExts := TypeRootAndExts(tt.typ)
if gotRoot != tt.wantRoot {
t.Errorf("TypeRootAndExts() root = %v, want %v", gotRoot, tt.wantRoot)
}
if len(gotExts) != len(tt.wantExts) {
t.Errorf("TypeRootAndExts() exts length = %v, want %v", len(gotExts), len(tt.wantExts))
return
}
for i, ext := range gotExts {
if ext != tt.wantExts[i] {
t.Errorf("TypeRootAndExts() exts[%d] = %v, want %v", i, ext, tt.wantExts[i])
}
}
})
}
}
// Test integration scenarios
func TestIntegration(t *testing.T) {
tests := []struct {
name string
typ Type
id string
wantPath string
wantID string
}{
{
name: "Model round trip",
typ: TypeModel,
id: "user__profile.admin",
wantPath: filepath.Join("models", "user.profile", "admin.mod.yao"),
wantID: "user__profile.admin",
},
{
name: "API round trip",
typ: TypeAPI,
id: "user.login",
wantPath: filepath.Join("apis", "user", "login.http.yao"),
wantID: "user.login",
},
{
name: "Complex nested path",
typ: TypeForm,
id: "admin__panel.user__management.edit",
wantPath: filepath.Join("forms", "admin.panel", "user.management", "edit.form.yao"),
wantID: "admin__panel.user__management.edit",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test ID to Path
path := ToPath(tt.typ, tt.id)
if path != tt.wantPath {
t.Errorf("ToPath() = %v, want %v", path, tt.wantPath)
}
// Test Path to ID
id := WithTypeToID(tt.typ, path)
if id != tt.wantID {
t.Errorf("WithTypeToID() = %v, want %v", id, tt.wantID)
}
// Test DetectType
detectedType := DetectType(path)
if detectedType != tt.typ {
t.Errorf("DetectType() = %v, want %v", detectedType, tt.typ)
}
})
}
}