diff --git a/handlers/common_handler.go b/handlers/common_handler.go index a616e96..97226c5 100644 --- a/handlers/common_handler.go +++ b/handlers/common_handler.go @@ -44,7 +44,7 @@ func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result { var conditionStatus string _, ok := e.conditions[defaultKey] for status, condition := range e.conditions { - if status != defaultKey { + if status != defaultKey && condition != nil { if condition.Match(data) { conditionStatus = status } diff --git a/handlers/html_handler.go b/handlers/html_handler.go index f31dd28..93cc84d 100644 --- a/handlers/html_handler.go +++ b/handlers/html_handler.go @@ -8,6 +8,7 @@ import ( "html/template" "os" + "github.com/oarkflow/jet" "github.com/oarkflow/mq" "github.com/oarkflow/mq/consts" "github.com/oarkflow/mq/dag" @@ -26,24 +27,30 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu templateStr, _ = data["template"].(string) templateFile, _ = data["template_file"].(string) ) - var templateData map[string]any if len(task.Payload) > 0 { if err := json.Unmarshal(task.Payload, &templateData); err != nil { - return mq.Result{Payload: task.Payload, Error: err, Ctx: ctx, ConditionStatus: "invalid"} + return mq.Result{Payload: task.Payload, Error: err, Ctx: ctx} } } if templateData == nil { templateData = make(map[string]any) } + delete(templateData, "html_content") + if c.Payload.Mapping != nil { + for k, v := range c.Payload.Mapping { + _, val := dag.GetVal(ctx, v, templateData) + templateData[k] = val + } + } templateData["task_id"] = ctx.Value("task_id") - var renderedHTML string var err error - + parser := jet.NewWithMemory(jet.WithDelims("{{", "}}")) switch { // 1. JSONSchema + HTML Template - case schemaFile != "" && templateStr != "": + case schemaFile != "" && templateStr != "" && templateFile == "": + fmt.Println("Using JSONSchema and inline HTML template", c.ID) if c.renderer == nil { renderer, err := renderer.GetFromFile(schemaFile, templateStr) if err != nil { @@ -53,7 +60,8 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu } renderedHTML, err = c.renderer.RenderFields(templateData) // 2. JSONSchema + HTML File - case schemaFile != "" && templateFile != "": + case schemaFile != "" && templateFile != "" && templateStr == "": + fmt.Println("Using JSONSchema and HTML file", c.ID) if c.renderer == nil { renderer, err := renderer.GetFromFile(schemaFile, "", templateFile) if err != nil { @@ -63,7 +71,8 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu } renderedHTML, err = c.renderer.RenderFields(templateData) // 3. Only JSONSchema - case schemaFile != "" || c.renderer != nil: + case (schemaFile != "" || c.renderer != nil) && templateStr == "" && templateFile == "": + fmt.Println("Using only JSONSchema", c.ID) if c.renderer == nil { renderer, err := renderer.GetFromFile(schemaFile, "") if err != nil { @@ -73,7 +82,8 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu } renderedHTML, err = c.renderer.RenderFields(templateData) // 4. Only HTML Template - case templateStr != "": + case templateStr != "" && templateFile == "" && schemaFile == "": + fmt.Println("Using inline HTML template", c.ID) tmpl, err := template.New("inline").Parse(templateStr) if err != nil { return mq.Result{Error: fmt.Errorf("failed to parse template: %v", err), Ctx: ctx} @@ -85,21 +95,16 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu } renderedHTML = buf.String() // 5. Only HTML File - case templateFile != "": + case templateFile != "" && templateStr == "" && schemaFile == "": + fmt.Println("Using HTML file", c.ID) fileContent, err := os.ReadFile(templateFile) if err != nil { return mq.Result{Error: fmt.Errorf("failed to read template file: %v", err), Ctx: ctx} } - tmpl, err := template.New("file").Parse(string(fileContent)) + renderedHTML, err = parser.ParseTemplate(string(fileContent), templateData) if err != nil { - return mq.Result{Error: fmt.Errorf("failed to parse template file: %v", err), Ctx: ctx} + return mq.Result{Error: err, Ctx: ctx} } - var buf bytes.Buffer - err = tmpl.Execute(&buf, templateData) - if err != nil { - return mq.Result{Error: fmt.Errorf("failed to execute template file: %v", err), Ctx: ctx} - } - renderedHTML = buf.String() default: return mq.Result{Error: fmt.Errorf("no valid rendering approach found"), Ctx: ctx} } diff --git a/services/examples/app/templates/basic.html b/services/examples/app/templates/basic.html new file mode 100644 index 0000000..f4ed557 --- /dev/null +++ b/services/examples/app/templates/basic.html @@ -0,0 +1,42 @@ + + + + + Basic Template + + + + + + +
+
+ {{form_groups}} +
+ {{form_buttons}} +
+
+
+ + + diff --git a/services/examples/app/templates/error.html b/services/examples/app/templates/error.html new file mode 100644 index 0000000..9935457 --- /dev/null +++ b/services/examples/app/templates/error.html @@ -0,0 +1,134 @@ + + + + + Email Error + + + + +
+
+

Email Processing Error

+ +
+ {{error_message}} +
+ + {{if error_field}} +
+ 🎯 Error Field: {{error_field}}
+ ⚡ Action Required: Please correct the highlighted field and try again.
+ 💡 Tip: Make sure all required fields are properly filled out. +
+ {{end}} + + {{if retry_suggested}} +
+ ⚠️ Temporary Issue: This appears to be a temporary system issue. + Please try sending your message again in a few moments.
+ 🔄 Auto-Retry: Our system will automatically retry failed deliveries. +
+ {{end}} + +
+ 🔄 Try Again + 📊 Check Status +
+ +
+ 🔄 DAG Error Handler | Email Notification Workflow Failed
+ Our advanced routing system ensures reliable message delivery. +
+
+ + + diff --git a/services/examples/json/login.json b/services/examples/json/login.json new file mode 100644 index 0000000..b97ac7e --- /dev/null +++ b/services/examples/json/login.json @@ -0,0 +1,71 @@ +{ + "name": "Login Flow", + "key": "login:flow", + "nodes": [ + { + "id": "LoginForm", + "first_node": true, + "node": "render-html", + "data": { + "additional_data": { + "schema_file": "login.json", + "template_file": "app/templates/basic.html" + } + } + }, + { + "id": "ValidateLogin", + "node": "condition", + "data": { + "mapping": { + "username": "username", + "password": "password" + }, + "additional_data": { + "conditions": { + "invalid": { + "id": "condition:invalid_login", + "node": "error-page", + "group": { + "reverse": true, + "filters": [ + { + "field": "username", + "operator": "eq", + "value": "admin" + }, + { + "field": "password", + "operator": "eq", + "value": "password" + } + ] + } + } + } + } + } + }, + { + "id": "error-page", + "node": "render-html", + "data": { + "mapping": { + "error_message": "eval.{{'Invalid login credentials.'}}", + "error_field": "eval.{{'username'}}", + "retry_suggested": "eval.{{true}}" + }, + "additional_data": { + "template_file": "app/templates/error.html" + } + } + } + ], + "edges": [ + { + "source": "LoginForm", + "target": [ "ValidateLogin" ] + } + ] + +} diff --git a/services/examples/json_email.go b/services/examples/json_email.go new file mode 100644 index 0000000..7522b7f --- /dev/null +++ b/services/examples/json_email.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/oarkflow/mq" + "github.com/oarkflow/mq/dag" + "github.com/oarkflow/mq/handlers" + "github.com/oarkflow/mq/services" +) + +func main() { + handlerBytes, err := os.ReadFile("json/login.json") + if err != nil { + panic(err) + } + var handler services.Handler + err = json.Unmarshal(handlerBytes, &handler) + if err != nil { + panic(err) + } + brokerAddr := ":8081" + flow := services.SetupHandler(handler, brokerAddr) + if flow.Error != nil { + fmt.Println("Error setting up handler:", flow.Error) + return + } + flow.Start(context.Background(), ":5000") +} + +func init() { + dag.AddHandler("render-html", func(id string) mq.Processor { return handlers.NewRenderHTMLNode(id) }) + dag.AddHandler("condition", func(id string) mq.Processor { return handlers.NewCondition(id) }) +} diff --git a/services/examples/login.json b/services/examples/login.json new file mode 100644 index 0000000..8f9cec1 --- /dev/null +++ b/services/examples/login.json @@ -0,0 +1,63 @@ +{ + "type": "object", + "properties": { + "username": { + "type": "string", + "title": "Username or Email", + "order": 1, + "ui": { + "element": "input", + "type": "text", + "class": "form-group", + "name": "username", + "placeholder": "Enter your username or email" + } + }, + "password": { + "type": "string", + "title": "Password", + "order": 2, + "ui": { + "element": "input", + "type": "password", + "class": "form-group", + "name": "password", + "placeholder": "Enter your password" + } + }, + "remember_me": { + "type": "boolean", + "title": "Remember Me", + "order": 3, + "ui": { + "element": "input", + "type": "checkbox", + "class": "form-check", + "name": "remember_me" + } + } + }, + "required": [ "username", "password" ], + "form": { + "class": "form-horizontal", + "action": "/process?task_id={{task_id}}&next=true", + "method": "POST", + "enctype": "application/x-www-form-urlencoded", + "groups": [ + { + "title": "Login Credentials", + "fields": [ "username", "password", "remember_me" ] + } + ], + "submit": { + "type": "submit", + "label": "Log In", + "class": "btn btn-primary" + }, + "reset": { + "type": "reset", + "label": "Clear", + "class": "btn btn-secondary" + } + } +} diff --git a/services/examples/schema.json b/services/examples/schema.json new file mode 100644 index 0000000..4195ea0 --- /dev/null +++ b/services/examples/schema.json @@ -0,0 +1,105 @@ +{ + "type": "object", + "properties": { + "first_name": { + "type": "string", + "title": "First Name", + "order": 1, + "ui": { + "element": "input", + "class": "form-group", + "name": "first_name" + } + }, + "last_name": { + "type": "string", + "title": "Last Name", + "order": 2, + "ui": { + "element": "input", + "class": "form-group", + "name": "last_name" + } + }, + "email": { + "type": "email", + "title": "Email Address", + "order": 3, + "ui": { + "element": "input", + "type": "email", + "class": "form-group", + "name": "email" + } + }, + "user_type": { + "type": "string", + "title": "User Type", + "order": 4, + "ui": { + "element": "select", + "class": "form-group", + "name": "user_type", + "options": [ "new", "premium", "standard" ] + } + }, + "priority": { + "type": "string", + "title": "Priority Level", + "order": 5, + "ui": { + "element": "select", + "class": "form-group", + "name": "priority", + "options": [ "low", "medium", "high", "urgent" ] + } + }, + "subject": { + "type": "string", + "title": "Subject", + "order": 6, + "ui": { + "element": "input", + "class": "form-group", + "name": "subject" + } + }, + "message": { + "type": "textarea", + "title": "Message", + "order": 7, + "ui": { + "element": "textarea", + "class": "form-group", + "name": "message" + } + } + }, + "required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ], + "form": { + "class": "form-horizontal", + "action": "/process?task_id={{task_id}}&next=true", + "method": "POST", + "enctype": "application/x-www-form-urlencoded", + "groups": [ + { + "title": "User Information", + "fields": [ "first_name", "last_name", "email" ] + }, + { + "title": "Ticket Details", + "fields": [ "user_type", "priority", "subject", "message" ] + } + ], + "submit": { + "type": "submit", + "label": "Submit", + "class": "btn btn-primary" + }, + "reset": { + "type": "reset", + "label": "Reset", + "class": "btn btn-secondary" + } + } +} diff --git a/services/setup.go b/services/setup.go index 982067a..b7fb058 100644 --- a/services/setup.go +++ b/services/setup.go @@ -77,10 +77,17 @@ func SetupHandler(handler Handler, brokerAddr string, async ...bool) *dag.DAG { return flow } +type FilterGroup struct { + Operator string `json:"operator"` + Reverse bool `json:"reverse"` + Filters []*filters.Filter `json:"filters"` +} + type Filter struct { - Filter *filters.Filter `json:"condition"` - Node string `json:"node"` - ID string `json:"id"` + Filter *filters.Filter `json:"condition"` + FilterGroup *FilterGroup `json:"group"` + Node string `json:"node"` + ID string `json:"id"` } func prepareNode(flow *dag.DAG, node Node) error { @@ -108,7 +115,25 @@ func prepareNode(flow *dag.DAG, node Node) error { conditions := make(map[string]dag.Condition) for key, cond := range fil { condition[key] = cond.Node - conditions[key] = cond.Filter + if cond.Filter != nil { + conditions[key] = cond.Filter + } else if cond.FilterGroup != nil { + cond.FilterGroup.Operator = strings.ToUpper(cond.FilterGroup.Operator) + if !slices.Contains([]string{"AND", "OR"}, cond.FilterGroup.Operator) { + cond.FilterGroup.Operator = "AND" + } + var fillers []filters.Condition + for _, f := range cond.FilterGroup.Filters { + if f != nil { + fillers = append(fillers, f) + } + } + conditions[key] = &filters.FilterGroup{ + Operator: filters.Boolean(cond.FilterGroup.Operator), + Reverse: cond.FilterGroup.Reverse, + Filters: fillers, + } + } } flow.AddCondition(node.ID, condition) nodeHandler.SetConditions(conditions)