diff --git a/pipe/context.go b/pipe/context.go index 74ae3ce4..124255f2 100644 --- a/pipe/context.go +++ b/pipe/context.go @@ -49,6 +49,47 @@ func Close(id string) { contexts.Delete(id) } +// Resume the context by id +func Resume(id string, args ...any) any { + ctx := Open(id) + v, err := ctx.resume(args...) + if err != nil { + exception.New("pipe: %s %s", 500, ctx.Name, err).Throw() + } + return v +} + +// resume the context by id +func (ctx *Context) resume(args ...any) (any, error) { + if ctx.current == nil { + return nil, ctx.Errorf("pipe %s has no nodes", ctx.Name) + } + + node := ctx.current + output, err := ctx.parseNodeOutput(node, args) + if err != nil { + return nil, node.Errorf(ctx, err.Error()) + } + + // Next node + next, eof, err := ctx.next() + if err != nil { + return nil, err + } + + // End of the pipe + if eof { + defer Close(ctx.id) + output, err := ctx.parseOutput() + if err != nil { + return nil, err + } + return output, nil + } + + return ctx.exec(next, anyToInput(output)) +} + // Run the pipe func (ctx *Context) Run(args ...any) any { v, err := ctx.Exec(args...) @@ -58,53 +99,6 @@ func (ctx *Context) Run(args ...any) any { return v } -// ID the context id -func (ctx *Context) ID() string { - return ctx.id -} - -// Next the next node -func (ctx *Context) Next() (*Node, bool, error) { - - if ctx.current == nil { - return nil, true, nil - } - - // if the goto is not empty, then goto the node - if ctx.current.Goto != "" { - data := ctx.data(ctx.current) - next, err := data.replaceString(ctx.current.Goto) - if err != nil { - return nil, false, err - } - - if next == "EOF" { - return nil, true, nil - } - - var has = false - ctx.current, has = ctx.mapping[next] - if !has { - return nil, false, ctx.Errorf("node %s not found", next) - } - return ctx.current, false, nil - } - - // continue to the next node - next := ctx.current.index + 1 - if next >= len(ctx.Nodes) { - return nil, true, nil - } - - ctx.current = &ctx.Nodes[next] - return ctx.current, false, nil -} - -// IsEOF check if the error is EOF -func IsEOF(err error) bool { - return err != nil && err.Error() == "EOF" -} - // Exec this is the entry point of the pipe func (ctx *Context) Exec(args ...any) (any, error) { if ctx.current == nil { @@ -150,17 +144,23 @@ func (ctx *Context) exec(node *Node, input Input) (output any, err error) { } case "user-input": - out, err = node.Render(ctx, input) + var pause bool = false + out, pause, err = node.Render(ctx, input) if err != nil { return nil, err } + // Pause the pipe waiting for user input + if pause { + return out, nil + } + default: return nil, node.Errorf(ctx, "type '%s' not support", node.Type) } // Execute the next node - next, eof, err := ctx.Next() + next, eof, err := ctx.next() if err != nil { return nil, err } @@ -180,6 +180,43 @@ func (ctx *Context) exec(node *Node, input Input) (output any, err error) { return ctx.exec(next, anyToInput(out)) } +// Next the next node +func (ctx *Context) next() (*Node, bool, error) { + + if ctx.current == nil { + return nil, true, nil + } + + // if the goto is not empty, then goto the node + if ctx.current.Goto != "" { + data := ctx.data(ctx.current) + next, err := data.replaceString(ctx.current.Goto) + if err != nil { + return nil, false, err + } + + if next == "EOF" { + return nil, true, nil + } + + var has = false + ctx.current, has = ctx.mapping[next] + if !has { + return nil, false, ctx.Errorf("node %s not found", next) + } + return ctx.current, false, nil + } + + // continue to the next node + next := ctx.current.index + 1 + if next >= len(ctx.Nodes) { + return nil, true, nil + } + + ctx.current = &ctx.Nodes[next] + return ctx.current, false, nil +} + // ParseNodeInput parse the node input func (ctx *Context) parseNodeInput(node *Node, input Input) (Input, error) { ctx.in[node] = input diff --git a/pipe/node.go b/pipe/node.go index af338002..57cbf5a0 100644 --- a/pipe/node.go +++ b/pipe/node.go @@ -209,18 +209,33 @@ func (node *Node) aiMergeHistory(ctx *Context, prompts []Prompt) []Prompt { } // Render Execute the user input -func (node *Node) Render(ctx *Context, input Input) (any, error) { +func (node *Node) Render(ctx *Context, input Input) (any, bool, error) { switch node.UI { case "cli": - return node.renderCli(ctx, input) + output, err := node.renderCli(ctx, input) + if err != nil { + return nil, false, err + } + return output, false, nil - case "web": + default: + input, err := ctx.parseNodeInput(node, input) + if err != nil { + return nil, true, err + } + + return ResumeContext{ + ID: ctx.id, + Input: input, + Node: node, + Data: ctx.data(node), + Type: node.Type, + UI: node.UI, + }, true, nil } - - return nil, fmt.Errorf("pipe: %s %s", ctx.Name, "node type error") } func (node *Node) renderCli(ctx *Context, input Input) (any, error) { @@ -255,12 +270,12 @@ func (node *Node) renderCli(ctx *Context, input Input) (any, error) { lines, err := cli.New(option).Render(input) if err != nil { - return nil, err + return nil, node.Errorf(ctx, err.Error()) } output, err := ctx.parseNodeOutput(node, lines) if err != nil { - return nil, err + return nil, node.Errorf(ctx, err.Error()) } return output, nil } diff --git a/pipe/pipe_test.go b/pipe/pipe_test.go index 27a4f20b..182fd8ce 100644 --- a/pipe/pipe_test.go +++ b/pipe/pipe_test.go @@ -2,7 +2,6 @@ package pipe import ( "context" - "fmt" "os" "testing" "time" @@ -15,10 +14,10 @@ import ( "github.com/yaoapp/yao/test" ) -func TestRun(t *testing.T) { +func TestRunCli(t *testing.T) { prepare(t) defer test.Clean() - translator, err := Get("translator") + translator, err := Get("cli.translator") if err != nil { t.Fatal(err) } @@ -31,7 +30,8 @@ func TestRun(t *testing.T) { With(context). WithGlobal(map[string]interface{}{"foo": "bar"}). WithSid(sid) - defer Close(ctx.ID()) + defer Close(ctx.id) + output, err := ctx.Exec(map[string]interface{}{"placeholder": "translate\nhello world"}) if err != nil { t.Fatal(err) @@ -43,16 +43,49 @@ func TestRun(t *testing.T) { assert.True(t, res.Has("output")) assert.True(t, res.Has("sid")) assert.True(t, res.Has("switch")) - assert.Equal(t, "bar", res.Get("global.foo")) assert.Equal(t, "translate\nhello world", res.Get("input[0].placeholder")) assert.Len(t, res.Get("switch"), 2) } +func TestRunWeb(t *testing.T) { + prepare(t) + defer test.Clean() + translator, err := Get("web.translator") + if err != nil { + t.Fatal(err) + } + + sid := session.ID() + context, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx := translator. + Create(). + With(context). + WithGlobal(map[string]interface{}{"foo": "bar"}). + WithSid(sid) + defer Close(ctx.id) + + web := ctx.Run("hello web world") + resume := web.(ResumeContext) + assert.Equal(t, Input{"hello web world"}, resume.Input) + + output := Resume(resume.ID, "translate", "hello web world") + + res := any.Of(output).Map().MapStrAny.Dot() + assert.True(t, res.Has("global")) + assert.True(t, res.Has("input")) + assert.True(t, res.Has("output")) + assert.True(t, res.Has("sid")) + assert.True(t, res.Has("switch")) + assert.Equal(t, "bar", res.Get("global.foo")) + assert.Equal(t, "hello web world", res.Get("input[0]")) + assert.Len(t, res.Get("switch"), 2) +} + func prepare(t *testing.T) { test.Prepare(t, config.Conf) mirror := os.Getenv("TEST_MOAPI_MIRROR") - fmt.Println(mirror) secret := os.Getenv("TEST_MOAPI_SECRET") share.App = share.AppInfo{ Moapi: share.Moapi{Channel: "stable", Mirrors: []string{mirror}, Secret: secret}, diff --git a/pipe/types.go b/pipe/types.go index f27d339a..b25be278 100644 --- a/pipe/types.go +++ b/pipe/types.go @@ -77,6 +77,16 @@ type Args []any // Data data for the template type Data map[string]interface{} +// ResumeContext the resume context +type ResumeContext struct { + ID string `json:"__id"` + Type string `json:"__type"` + UI string `json:"__ui"` + Input Input `json:"input"` + Node *Node `json:"node"` + Data Data `json:"data"` +} + // AutoFill the autofill type AutoFill struct { Value any `json:"value"` diff --git a/pipe/ui/web/web.go b/pipe/ui/web/web.go deleted file mode 100644 index e41d899f..00000000 --- a/pipe/ui/web/web.go +++ /dev/null @@ -1,12 +0,0 @@ -package web - -// Web the web UI -type Web struct{} - -// Option the web option -type Option struct{} - -// Render the Web UI -func (web *Web) Render(args []any, option Option) error { - return nil -}