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
+
+
+
+
+
+
+
+
+
+
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}}
+
+
+
+
+ 🔄 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)