package main
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/oarkflow/json"
"github.com/oarkflow/jet"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/consts"
"github.com/oarkflow/mq/dag"
)
// loginSubDAG creates a login sub-DAG with page for authentication
func loginSubDAG() *dag.DAG {
login := dag.NewDAG("Login Sub DAG", "login-sub-dag", func(taskID string, result mq.Result) {
fmt.Printf("Login Sub DAG Final result for task %s: %s\n", taskID, string(result.Payload))
}, mq.WithSyncMode(true))
login.
AddNode(dag.Page, "Login Page", "login-page", &LoginPage{}).
AddNode(dag.Function, "Verify Credentials", "verify-credentials", &VerifyCredentials{}).
AddNode(dag.Function, "Generate Token", "generate-token", &GenerateToken{}).
AddEdge(dag.Simple, "Login to Verify", "login-page", "verify-credentials").
AddEdge(dag.Simple, "Verify to Token", "verify-credentials", "generate-token")
return login
}
func main() {
flow := dag.NewDAG("Complex Phone Processing DAG with Pages", "complex-phone-dag", func(taskID string, result mq.Result) {
fmt.Printf("Complex DAG Final result for task %s: %s\n", taskID, string(result.Payload))
})
flow.ConfigureMemoryStorage()
// Main nodes - Login process as individual nodes (not sub-DAG) for proper page serving
flow.AddNode(dag.Page, "Initialize", "init", &Initialize{}, true)
flow.AddNode(dag.Page, "Login Page", "login-page", &LoginPage{})
flow.AddNode(dag.Function, "Verify Credentials", "verify-credentials", &VerifyCredentials{})
flow.AddNode(dag.Function, "Generate Token", "generate-token", &GenerateToken{})
flow.AddNode(dag.Page, "Upload Phone Data", "upload-page", &UploadPhoneDataPage{})
flow.AddNode(dag.Function, "Parse Phone Numbers", "parse-phones", &ParsePhoneNumbers{})
flow.AddNode(dag.Function, "Phone Loop", "phone-loop", &PhoneLoop{})
flow.AddNode(dag.Function, "Validate Phone", "validate-phone", &ValidatePhone{})
flow.AddNode(dag.Function, "Send Welcome SMS", "send-welcome", &SendWelcomeSMS{})
flow.AddNode(dag.Function, "Collect Valid Phones", "collect-valid", &CollectValidPhones{})
flow.AddNode(dag.Function, "Collect Invalid Phones", "collect-invalid", &CollectInvalidPhones{})
flow.AddNode(dag.Function, "Generate Report", "generate-report", &GenerateReport{})
flow.AddNode(dag.Function, "Send Summary Email", "send-summary", &SendSummaryEmail{})
flow.AddNode(dag.Function, "Final Cleanup", "cleanup", &FinalCleanup{})
// Edges - Connect login flow individually
flow.AddEdge(dag.Simple, "Init to Login", "init", "login-page")
flow.AddEdge(dag.Simple, "Login to Verify", "login-page", "verify-credentials")
flow.AddEdge(dag.Simple, "Verify to Token", "verify-credentials", "generate-token")
flow.AddEdge(dag.Simple, "Token to Upload", "generate-token", "upload-page")
flow.AddEdge(dag.Simple, "Upload to Parse", "upload-page", "parse-phones")
flow.AddEdge(dag.Simple, "Parse to Loop", "parse-phones", "phone-loop")
flow.AddEdge(dag.Iterator, "Loop over phones", "phone-loop", "validate-phone")
flow.AddCondition("validate-phone", map[string]string{"valid": "send-welcome", "invalid": "collect-invalid"})
flow.AddEdge(dag.Simple, "Welcome to Collect", "send-welcome", "collect-valid")
flow.AddEdge(dag.Simple, "Invalid to Collect", "collect-invalid", "collect-valid")
flow.AddEdge(dag.Simple, "Loop to Report", "phone-loop", "generate-report")
flow.AddEdge(dag.Simple, "Report to Summary", "generate-report", "send-summary")
flow.AddEdge(dag.Simple, "Summary to Cleanup", "send-summary", "cleanup")
// Check for DAG errors
// if flow.Error != nil {
// fmt.Printf("DAG Error: %v\n", flow.Error)
// panic(flow.Error)
// }
fmt.Println("Starting Complex Phone Processing DAG server on http://0.0.0.0:8080")
fmt.Println("Navigate to the URL to access the login page")
flow.Start(context.Background(), ":8080")
}
// Task implementations
type Initialize struct {
dag.Operation
}
func (p *Initialize) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
data = make(map[string]interface{})
}
data["initialized"] = true
data["timestamp"] = "2025-09-19T12:00:00Z"
// Add sample phone data for testing
sampleCSV := `name,phone
John Doe,+1234567890
Jane Smith,0987654321
Bob Johnson,1555123456
Alice Brown,invalid-phone
Charlie Wilson,+441234567890`
data["phone_data"] = map[string]interface{}{
"content": sampleCSV,
"format": "csv",
"source": "sample_data",
"created_at": "2025-09-19T12:00:00Z",
}
// Generate a task ID for this workflow instance
taskID := "workflow-" + fmt.Sprintf("%d", time.Now().Unix())
// Since this is a page node, show a welcome page that auto-redirects to login
htmlContent := `
Phone Processing System
📱 Phone Processing System
Welcome to our advanced phone number processing workflow
Features:
- CSV/JSON file upload support
- Phone number validation and formatting
- Automated welcome SMS sending
- Invalid number filtering
- Comprehensive processing reports
Initializing workflow...
Task ID: ` + taskID + `
Redirecting to login page in 3 seconds...
`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(htmlContent, map[string]any{})
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
resultData := map[string]any{
"html_content": rs,
"step": "initialize",
"data": data,
}
resultPayload, _ := json.Marshal(resultData)
return mq.Result{Payload: resultPayload, Ctx: ctx}
}
type LoginPage struct {
dag.Operation
}
func (p *LoginPage) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
// Check if this is a form submission
var inputData map[string]interface{}
if len(task.Payload) > 0 {
if err := json.Unmarshal(task.Payload, &inputData); err == nil {
// Check if we have form data (username/password)
if formData, ok := inputData["form"].(map[string]interface{}); ok {
// This is a form submission, pass it through for verification
credentials := map[string]interface{}{
"username": formData["username"],
"password": formData["password"],
}
inputData["credentials"] = credentials
updatedPayload, _ := json.Marshal(inputData)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
}
}
// Otherwise, show the form
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
data = make(map[string]interface{})
}
// HTML content for login page
htmlContent := `
Phone Processing System - Login
`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(htmlContent, map[string]any{})
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
resultData := map[string]any{
"html_content": rs,
"step": "login",
"data": data,
}
resultPayload, _ := json.Marshal(resultData)
return mq.Result{
Payload: resultPayload,
Ctx: ctx,
}
}
type VerifyCredentials struct {
dag.Operation
}
func (p *VerifyCredentials) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("VerifyCredentials Error: %s", err.Error()), Ctx: ctx}
}
credentials, ok := data["credentials"].(map[string]interface{})
if !ok {
return mq.Result{Error: fmt.Errorf("credentials not found"), Ctx: ctx}
}
username, _ := credentials["username"].(string)
password, _ := credentials["password"].(string)
// Simple verification logic
if username == "admin" && password == "password123" {
data["authenticated"] = true
data["user_role"] = "administrator"
} else {
data["authenticated"] = false
data["error"] = "Invalid credentials"
}
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type GenerateToken struct {
dag.Operation
}
func (p *GenerateToken) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("GenerateToken Error: %s", err.Error()), Ctx: ctx}
}
if authenticated, ok := data["authenticated"].(bool); ok && authenticated {
data["auth_token"] = "jwt_token_123456789"
data["token_expires"] = "2025-09-19T13:00:00Z"
}
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type UploadPhoneDataPage struct {
dag.Operation
}
func (p *UploadPhoneDataPage) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
// Check if this is a form submission
var inputData map[string]interface{}
if len(task.Payload) > 0 {
if err := json.Unmarshal(task.Payload, &inputData); err == nil {
// Check if we have form data (phone_data)
if formData, ok := inputData["form"].(map[string]interface{}); ok {
// This is a form submission, pass it through for processing
if phoneData, exists := formData["phone_data"]; exists && phoneData != "" {
inputData["phone_data"] = map[string]interface{}{
"content": phoneData.(string),
"format": "csv",
"source": "user_input",
"created_at": "2025-09-19T12:00:00Z",
}
}
updatedPayload, _ := json.Marshal(inputData)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
}
}
// Otherwise, show the form
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
data = make(map[string]interface{})
}
// HTML content for upload page
htmlContent := `
Phone Processing System - Upload Data
`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(htmlContent, map[string]any{})
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
resultData := map[string]any{
"html_content": rs,
"step": "upload",
"data": data,
}
resultPayload, _ := json.Marshal(resultData)
return mq.Result{
Payload: resultPayload,
Ctx: ctx,
}
}
type ParsePhoneNumbers struct {
dag.Operation
}
func (p *ParsePhoneNumbers) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("ParsePhoneNumbers Error: %s", err.Error()), Ctx: ctx}
}
phoneData, ok := data["phone_data"].(map[string]interface{})
if !ok {
return mq.Result{Error: fmt.Errorf("phone_data not found"), Ctx: ctx}
}
content, ok := phoneData["content"].(string)
if !ok {
return mq.Result{Error: fmt.Errorf("phone data content not found"), Ctx: ctx}
}
var phones []map[string]interface{}
// Parse CSV content
lines := strings.Split(content, "\n")
if len(lines) > 1 {
headers := strings.Split(lines[0], ",")
for i := 1; i < len(lines); i++ {
if lines[i] == "" {
continue
}
values := strings.Split(lines[i], ",")
if len(values) >= len(headers) {
phone := make(map[string]interface{})
for j, header := range headers {
phone[strings.TrimSpace(header)] = strings.TrimSpace(values[j])
}
phones = append(phones, phone)
}
}
}
data["parsed_phones"] = phones
data["total_phones"] = len(phones)
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type PhoneLoop struct {
dag.Operation
}
func (p *PhoneLoop) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("PhoneLoop Error: %s", err.Error()), Ctx: ctx}
}
// Extract parsed phones for iteration
if phones, ok := data["parsed_phones"].([]interface{}); ok {
updatedPayload, _ := json.Marshal(phones)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
return mq.Result{Payload: task.Payload, Ctx: ctx}
}
type ValidatePhone struct {
dag.Operation
}
func (p *ValidatePhone) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var phone map[string]interface{}
if err := json.Unmarshal(task.Payload, &phone); err != nil {
return mq.Result{Error: fmt.Errorf("ValidatePhone Error: %s", err.Error()), Ctx: ctx}
}
phoneStr, ok := phone["phone"].(string)
if !ok {
return mq.Result{Payload: task.Payload, ConditionStatus: "invalid", Ctx: ctx}
}
// Simple phone validation regex (supports international format)
validPhone := regexp.MustCompile(`^\+?[1-9]\d{1,14}$`)
if validPhone.MatchString(phoneStr) {
phone["valid"] = true
phone["formatted_phone"] = phoneStr
return mq.Result{Payload: task.Payload, ConditionStatus: "valid", Ctx: ctx}
}
phone["valid"] = false
phone["validation_error"] = "Invalid phone number format"
return mq.Result{Payload: task.Payload, ConditionStatus: "invalid", Ctx: ctx}
}
type SendWelcomeSMS struct {
dag.Operation
}
func (p *SendWelcomeSMS) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var phone map[string]interface{}
if err := json.Unmarshal(task.Payload, &phone); err != nil {
return mq.Result{Error: fmt.Errorf("SendWelcomeSMS Error: %s", err.Error()), Ctx: ctx}
}
phoneStr, ok := phone["phone"].(string)
if !ok {
return mq.Result{Error: fmt.Errorf("phone number not found"), Ctx: ctx}
}
// Simulate sending welcome SMS
phone["welcome_sent"] = true
phone["welcome_message"] = "Welcome! Your phone number has been verified."
phone["sent_at"] = "2025-09-19T12:10:00Z"
fmt.Printf("Welcome SMS sent to %s\n", phoneStr)
updatedPayload, _ := json.Marshal(phone)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type CollectValidPhones struct {
dag.Operation
}
func (p *CollectValidPhones) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
// This node collects all processed phone results
return mq.Result{Payload: task.Payload, Ctx: ctx}
}
type CollectInvalidPhones struct {
dag.Operation
}
func (p *CollectInvalidPhones) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var phone map[string]interface{}
if err := json.Unmarshal(task.Payload, &phone); err != nil {
return mq.Result{Error: fmt.Errorf("CollectInvalidPhones Error: %s", err.Error()), Ctx: ctx}
}
phone["discarded"] = true
phone["discard_reason"] = "Invalid phone number"
fmt.Printf("Invalid phone discarded: %v\n", phone["phone"])
updatedPayload, _ := json.Marshal(phone)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type GenerateReport struct {
dag.Operation
}
func (p *GenerateReport) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
// If it's an array, wrap it in a map
var arr []interface{}
if err2 := json.Unmarshal(task.Payload, &arr); err2 != nil {
return mq.Result{Error: fmt.Errorf("GenerateReport Error: %s", err.Error()), Ctx: ctx}
}
data = map[string]interface{}{
"processed_results": arr,
}
}
// Generate processing report
validCount := 0
invalidCount := 0
if results, ok := data["processed_results"].([]interface{}); ok {
for _, result := range results {
if resultMap, ok := result.(map[string]interface{}); ok {
if _, isValid := resultMap["welcome_sent"]; isValid {
validCount++
} else if _, isInvalid := resultMap["discarded"]; isInvalid {
invalidCount++
}
}
}
}
report := map[string]interface{}{
"total_processed": validCount + invalidCount,
"valid_phones": validCount,
"invalid_phones": invalidCount,
"processed_at": "2025-09-19T12:15:00Z",
"success": true,
}
data["report"] = report
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type SendSummaryEmail struct {
dag.Operation
}
func (p *SendSummaryEmail) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("SendSummaryEmail Error: %s", err.Error()), Ctx: ctx}
}
// Simulate sending summary email
data["summary_email_sent"] = true
data["summary_recipient"] = "admin@company.com"
data["summary_sent_at"] = "2025-09-19T12:20:00Z"
fmt.Println("Summary email sent to admin")
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type FinalCleanup struct {
dag.Operation
}
func (p *FinalCleanup) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]interface{}
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("FinalCleanup Error: %s", err.Error()), Ctx: ctx}
}
// Perform final cleanup
data["completed"] = true
data["completed_at"] = "2025-09-19T12:25:00Z"
data["workflow_status"] = "success"
fmt.Println("Workflow completed successfully")
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}